public final class

MediaItem

extends java.lang.Object

implements Bundleable

 java.lang.Object

↳androidx.media3.common.MediaItem

Gradle dependencies

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

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

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

Overview

Representation of a media item.

Summary

Fields
public final MediaItem.ClippingConfigurationclippingConfiguration

The clipping properties.

public final MediaItem.ClippingPropertiesclippingProperties

public static final Bundleable.Creator<MediaItem>CREATOR

Object that can restore MediaItem from a .

public static final java.lang.StringDEFAULT_MEDIA_ID

The default media ID that is used if the media ID is not explicitly set by MediaItem.Builder.setMediaId(String).

public static final MediaItemEMPTY

Empty MediaItem.

public final MediaItem.LiveConfigurationliveConfiguration

The live playback configuration.

public final MediaItem.LocalConfigurationlocalConfiguration

Optional configuration for local playback.

public final java.lang.StringmediaId

Identifies the media item.

public final MediaMetadatamediaMetadata

The media metadata.

public final MediaItem.PlaybackPropertiesplaybackProperties

Methods
public MediaItem.BuilderbuildUpon()

Returns a MediaItem.Builder initialized with the values of this instance.

public booleanequals(java.lang.Object obj)

public static MediaItemfromUri(java.lang.String uri)

Creates a MediaItem for the given URI.

public static MediaItemfromUri(Uri uri)

Creates a MediaItem for the given .

public inthashCode()

public BundletoBundle()

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

Fields

public static final java.lang.String DEFAULT_MEDIA_ID

The default media ID that is used if the media ID is not explicitly set by MediaItem.Builder.setMediaId(String).

public static final MediaItem EMPTY

Empty MediaItem.

public final java.lang.String mediaId

Identifies the media item.

public final MediaItem.LocalConfiguration localConfiguration

Optional configuration for local playback. May be null if shared over process boundaries.

public final MediaItem.PlaybackProperties playbackProperties

Deprecated: Use MediaItem.localConfiguration instead.

public final MediaItem.LiveConfiguration liveConfiguration

The live playback configuration.

public final MediaMetadata mediaMetadata

The media metadata.

public final MediaItem.ClippingConfiguration clippingConfiguration

The clipping properties.

public final MediaItem.ClippingProperties clippingProperties

Deprecated: Use MediaItem.clippingConfiguration instead.

public static final Bundleable.Creator<MediaItem> CREATOR

Object that can restore MediaItem from a .

The MediaItem.localConfiguration of a restored instance will always be null.

Methods

public static MediaItem fromUri(java.lang.String uri)

Creates a MediaItem for the given URI.

Parameters:

uri: The URI.

Returns:

An MediaItem for the given URI.

public static MediaItem fromUri(Uri uri)

Creates a MediaItem for the given .

Parameters:

uri: The .

Returns:

An MediaItem for the given URI.

public MediaItem.Builder buildUpon()

Returns a MediaItem.Builder initialized with the values of this instance.

public boolean equals(java.lang.Object obj)

public int hashCode()

public Bundle toBundle()

It omits the MediaItem.localConfiguration field. The MediaItem.localConfiguration of an instance restored by MediaItem.CREATOR will always be null.

Source

/*
 * Copyright 2020 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.common;

import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static java.lang.annotation.ElementType.TYPE_USE;

import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/** Representation of a media item. */
public final class MediaItem implements Bundleable {

  /**
   * Creates a {@link MediaItem} for the given URI.
   *
   * @param uri The URI.
   * @return An {@link MediaItem} for the given URI.
   */
  public static MediaItem fromUri(String uri) {
    return new MediaItem.Builder().setUri(uri).build();
  }

  /**
   * Creates a {@link MediaItem} for the given {@link Uri URI}.
   *
   * @param uri The {@link Uri uri}.
   * @return An {@link MediaItem} for the given URI.
   */
  public static MediaItem fromUri(Uri uri) {
    return new MediaItem.Builder().setUri(uri).build();
  }

  /** A builder for {@link MediaItem} instances. */
  public static final class Builder {

    @Nullable private String mediaId;
    @Nullable private Uri uri;
    @Nullable private String mimeType;
    // TODO: Change this to ClippingProperties once all the deprecated individual setters are
    // removed.
    private ClippingConfiguration.Builder clippingConfiguration;
    // TODO: Change this to @Nullable DrmConfiguration once all the deprecated individual setters
    // are removed.
    private DrmConfiguration.Builder drmConfiguration;
    private List<StreamKey> streamKeys;
    @Nullable private String customCacheKey;
    private ImmutableList<SubtitleConfiguration> subtitleConfigurations;
    @Nullable private AdsConfiguration adsConfiguration;
    @Nullable private Object tag;
    @Nullable private MediaMetadata mediaMetadata;
    // TODO: Change this to LiveConfiguration once all the deprecated individual setters
    // are removed.
    private LiveConfiguration.Builder liveConfiguration;

    /** Creates a builder. */
    @SuppressWarnings("deprecation") // Temporarily uses DrmConfiguration.Builder() constructor.
    public Builder() {
      clippingConfiguration = new ClippingConfiguration.Builder();
      drmConfiguration = new DrmConfiguration.Builder();
      streamKeys = Collections.emptyList();
      subtitleConfigurations = ImmutableList.of();
      liveConfiguration = new LiveConfiguration.Builder();
    }

    private Builder(MediaItem mediaItem) {
      this();
      clippingConfiguration = mediaItem.clippingConfiguration.buildUpon();
      mediaId = mediaItem.mediaId;
      mediaMetadata = mediaItem.mediaMetadata;
      liveConfiguration = mediaItem.liveConfiguration.buildUpon();
      @Nullable LocalConfiguration localConfiguration = mediaItem.localConfiguration;
      if (localConfiguration != null) {
        customCacheKey = localConfiguration.customCacheKey;
        mimeType = localConfiguration.mimeType;
        uri = localConfiguration.uri;
        streamKeys = localConfiguration.streamKeys;
        subtitleConfigurations = localConfiguration.subtitleConfigurations;
        tag = localConfiguration.tag;
        drmConfiguration =
            localConfiguration.drmConfiguration != null
                ? localConfiguration.drmConfiguration.buildUpon()
                : new DrmConfiguration.Builder();
        adsConfiguration = localConfiguration.adsConfiguration;
      }
    }

    /**
     * Sets the optional media ID which identifies the media item.
     *
     * <p>By default {@link #DEFAULT_MEDIA_ID} is used.
     */
    public Builder setMediaId(String mediaId) {
      this.mediaId = checkNotNull(mediaId);
      return this;
    }

    /**
     * Sets the optional URI.
     *
     * <p>If {@code uri} is null or unset then no {@link LocalConfiguration} object is created
     * during {@link #build()} and no other {@code Builder} methods that would populate {@link
     * MediaItem#localConfiguration} should be called.
     */
    public Builder setUri(@Nullable String uri) {
      return setUri(uri == null ? null : Uri.parse(uri));
    }

    /**
     * Sets the optional URI.
     *
     * <p>If {@code uri} is null or unset then no {@link LocalConfiguration} object is created
     * during {@link #build()} and no other {@code Builder} methods that would populate {@link
     * MediaItem#localConfiguration} should be called.
     */
    public Builder setUri(@Nullable Uri uri) {
      this.uri = uri;
      return this;
    }

    /**
     * Sets the optional MIME type.
     *
     * <p>The MIME type may be used as a hint for inferring the type of the media item.
     *
     * <p>This method should only be called if {@link #setUri} is passed a non-null value.
     *
     * @param mimeType The MIME type.
     */
    public Builder setMimeType(@Nullable String mimeType) {
      this.mimeType = mimeType;
      return this;
    }

    /** Sets the {@link ClippingConfiguration}, defaults to {@link ClippingConfiguration#UNSET}. */
    public Builder setClippingConfiguration(ClippingConfiguration clippingConfiguration) {
      this.clippingConfiguration = clippingConfiguration.buildUpon();
      return this;
    }

    /**
     * @deprecated Use {@link #setClippingConfiguration(ClippingConfiguration)} and {@link
     *     ClippingConfiguration.Builder#setStartPositionMs(long)} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setClipStartPositionMs(@IntRange(from = 0) long startPositionMs) {
      clippingConfiguration.setStartPositionMs(startPositionMs);
      return this;
    }

    /**
     * @deprecated Use {@link #setClippingConfiguration(ClippingConfiguration)} and {@link
     *     ClippingConfiguration.Builder#setEndPositionMs(long)} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setClipEndPositionMs(long endPositionMs) {
      clippingConfiguration.setEndPositionMs(endPositionMs);
      return this;
    }

    /**
     * @deprecated Use {@link #setClippingConfiguration(ClippingConfiguration)} and {@link
     *     ClippingConfiguration.Builder#setRelativeToLiveWindow(boolean)} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setClipRelativeToLiveWindow(boolean relativeToLiveWindow) {
      clippingConfiguration.setRelativeToLiveWindow(relativeToLiveWindow);
      return this;
    }

    /**
     * @deprecated Use {@link #setClippingConfiguration(ClippingConfiguration)} and {@link
     *     ClippingConfiguration.Builder#setRelativeToDefaultPosition(boolean)} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setClipRelativeToDefaultPosition(boolean relativeToDefaultPosition) {
      clippingConfiguration.setRelativeToDefaultPosition(relativeToDefaultPosition);
      return this;
    }

    /**
     * @deprecated Use {@link #setClippingConfiguration(ClippingConfiguration)} and {@link
     *     ClippingConfiguration.Builder#setStartsAtKeyFrame(boolean)} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setClipStartsAtKeyFrame(boolean startsAtKeyFrame) {
      clippingConfiguration.setStartsAtKeyFrame(startsAtKeyFrame);
      return this;
    }

    /** Sets the optional DRM configuration. */
    public Builder setDrmConfiguration(@Nullable DrmConfiguration drmConfiguration) {
      this.drmConfiguration =
          drmConfiguration != null ? drmConfiguration.buildUpon() : new DrmConfiguration.Builder();
      return this;
    }

    /**
     * @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
     *     DrmConfiguration.Builder#setLicenseUri(Uri)} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setDrmLicenseUri(@Nullable Uri licenseUri) {
      drmConfiguration.setLicenseUri(licenseUri);
      return this;
    }

    /**
     * @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
     *     DrmConfiguration.Builder#setLicenseUri(String)} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setDrmLicenseUri(@Nullable String licenseUri) {
      drmConfiguration.setLicenseUri(licenseUri);
      return this;
    }

    /**
     * @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
     *     DrmConfiguration.Builder#setLicenseRequestHeaders(Map)} instead. Note that {@link
     *     DrmConfiguration.Builder#setLicenseRequestHeaders(Map)} doesn't accept null, use an empty
     *     map to clear the headers.
     */
    @UnstableApi
    @Deprecated
    public Builder setDrmLicenseRequestHeaders(
        @Nullable Map<String, String> licenseRequestHeaders) {
      drmConfiguration.setLicenseRequestHeaders(
          licenseRequestHeaders != null ? licenseRequestHeaders : ImmutableMap.of());
      return this;
    }

    /**
     * @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and pass the {@code uuid} to
     *     {@link DrmConfiguration.Builder#Builder(UUID)} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setDrmUuid(@Nullable UUID uuid) {
      drmConfiguration.setNullableScheme(uuid);
      return this;
    }

    /**
     * @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
     *     DrmConfiguration.Builder#setMultiSession(boolean)} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setDrmMultiSession(boolean multiSession) {
      drmConfiguration.setMultiSession(multiSession);
      return this;
    }

    /**
     * @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
     *     DrmConfiguration.Builder#setForceDefaultLicenseUri(boolean)} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setDrmForceDefaultLicenseUri(boolean forceDefaultLicenseUri) {
      drmConfiguration.setForceDefaultLicenseUri(forceDefaultLicenseUri);
      return this;
    }

    /**
     * @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
     *     DrmConfiguration.Builder#setPlayClearContentWithoutKey(boolean)} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setDrmPlayClearContentWithoutKey(boolean playClearContentWithoutKey) {
      drmConfiguration.setPlayClearContentWithoutKey(playClearContentWithoutKey);
      return this;
    }

    /**
     * @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
     *     DrmConfiguration.Builder#forceSessionsForAudioAndVideoTracks(boolean)} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setDrmSessionForClearPeriods(boolean sessionForClearPeriods) {
      drmConfiguration.forceSessionsForAudioAndVideoTracks(sessionForClearPeriods);
      return this;
    }

    /**
     * @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
     *     DrmConfiguration.Builder#setForcedSessionTrackTypes(List)} instead. Note that {@link
     *     DrmConfiguration.Builder#setForcedSessionTrackTypes(List)} doesn't accept null, use an
     *     empty list to clear the contents.
     */
    @UnstableApi
    @Deprecated
    public Builder setDrmSessionForClearTypes(
        @Nullable List<@C.TrackType Integer> sessionForClearTypes) {
      drmConfiguration.setForcedSessionTrackTypes(
          sessionForClearTypes != null ? sessionForClearTypes : ImmutableList.of());
      return this;
    }

    /**
     * @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
     *     DrmConfiguration.Builder#setKeySetId(byte[])} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setDrmKeySetId(@Nullable byte[] keySetId) {
      drmConfiguration.setKeySetId(keySetId);
      return this;
    }

    /**
     * Sets the optional stream keys by which the manifest is filtered (only used for adaptive
     * streams).
     *
     * <p>{@code null} or an empty {@link List} can be used for a reset.
     *
     * <p>If {@link #setUri} is passed a non-null {@code uri}, the stream keys are used to create a
     * {@link LocalConfiguration} object. Otherwise they will be ignored.
     */
    @UnstableApi
    public Builder setStreamKeys(@Nullable List<StreamKey> streamKeys) {
      this.streamKeys =
          streamKeys != null && !streamKeys.isEmpty()
              ? Collections.unmodifiableList(new ArrayList<>(streamKeys))
              : Collections.emptyList();
      return this;
    }

    /**
     * Sets the optional custom cache key (only used for progressive streams).
     *
     * <p>This method should only be called if {@link #setUri} is passed a non-null value.
     */
    @UnstableApi
    public Builder setCustomCacheKey(@Nullable String customCacheKey) {
      this.customCacheKey = customCacheKey;
      return this;
    }

    /**
     * @deprecated Use {@link #setSubtitleConfigurations(List)} instead. Note that {@link
     *     #setSubtitleConfigurations(List)} doesn't accept null, use an empty list to clear the
     *     contents.
     */
    @UnstableApi
    @Deprecated
    public Builder setSubtitles(@Nullable List<Subtitle> subtitles) {
      this.subtitleConfigurations =
          subtitles != null ? ImmutableList.copyOf(subtitles) : ImmutableList.of();
      return this;
    }

    /**
     * Sets the optional subtitles.
     *
     * <p>This method should only be called if {@link #setUri} is passed a non-null value.
     */
    public Builder setSubtitleConfigurations(List<SubtitleConfiguration> subtitleConfigurations) {
      this.subtitleConfigurations = ImmutableList.copyOf(subtitleConfigurations);
      return this;
    }

    /**
     * Sets the optional {@link AdsConfiguration}.
     *
     * <p>This method should only be called if {@link #setUri} is passed a non-null value.
     */
    public Builder setAdsConfiguration(@Nullable AdsConfiguration adsConfiguration) {
      this.adsConfiguration = adsConfiguration;
      return this;
    }

    /**
     * @deprecated Use {@link #setAdsConfiguration(AdsConfiguration)}, parse the {@code adTagUri}
     *     with {@link Uri#parse(String)} and pass the result to {@link
     *     AdsConfiguration.Builder#Builder(Uri)} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setAdTagUri(@Nullable String adTagUri) {
      return setAdTagUri(adTagUri != null ? Uri.parse(adTagUri) : null);
    }

    /**
     * @deprecated Use {@link #setAdsConfiguration(AdsConfiguration)} and pass the {@code adTagUri}
     *     to {@link AdsConfiguration.Builder#Builder(Uri)} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setAdTagUri(@Nullable Uri adTagUri) {
      return setAdTagUri(adTagUri, /* adsId= */ null);
    }

    /**
     * @deprecated Use {@link #setAdsConfiguration(AdsConfiguration)}, pass the {@code adTagUri} to
     *     {@link AdsConfiguration.Builder#Builder(Uri)} and the {@code adsId} to {@link
     *     AdsConfiguration.Builder#setAdsId(Object)} instead.
     */
    @UnstableApi
    @Deprecated
    public Builder setAdTagUri(@Nullable Uri adTagUri, @Nullable Object adsId) {
      this.adsConfiguration =
          adTagUri != null ? new AdsConfiguration.Builder(adTagUri).setAdsId(adsId).build() : null;
      return this;
    }

    /** Sets the {@link LiveConfiguration}. Defaults to {@link LiveConfiguration#UNSET}. */
    public Builder setLiveConfiguration(LiveConfiguration liveConfiguration) {
      this.liveConfiguration = liveConfiguration.buildUpon();
      return this;
    }

    /**
     * @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
     *     LiveConfiguration.Builder#setTargetOffsetMs(long)}.
     */
    @UnstableApi
    @Deprecated
    public Builder setLiveTargetOffsetMs(long liveTargetOffsetMs) {
      liveConfiguration.setTargetOffsetMs(liveTargetOffsetMs);
      return this;
    }

    /**
     * @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
     *     LiveConfiguration.Builder#setMinOffsetMs(long)}.
     */
    @UnstableApi
    @Deprecated
    public Builder setLiveMinOffsetMs(long liveMinOffsetMs) {
      liveConfiguration.setMinOffsetMs(liveMinOffsetMs);
      return this;
    }

    /**
     * @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
     *     LiveConfiguration.Builder#setMaxOffsetMs(long)}.
     */
    @UnstableApi
    @Deprecated
    public Builder setLiveMaxOffsetMs(long liveMaxOffsetMs) {
      liveConfiguration.setMaxOffsetMs(liveMaxOffsetMs);
      return this;
    }

    /**
     * @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
     *     LiveConfiguration.Builder#setMinPlaybackSpeed(float)}.
     */
    @UnstableApi
    @Deprecated
    public Builder setLiveMinPlaybackSpeed(float minPlaybackSpeed) {
      liveConfiguration.setMinPlaybackSpeed(minPlaybackSpeed);
      return this;
    }

    /**
     * @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
     *     LiveConfiguration.Builder#setMaxPlaybackSpeed(float)}.
     */
    @UnstableApi
    @Deprecated
    public Builder setLiveMaxPlaybackSpeed(float maxPlaybackSpeed) {
      liveConfiguration.setMaxPlaybackSpeed(maxPlaybackSpeed);
      return this;
    }

    /**
     * Sets the optional tag for custom attributes. The tag for the media source which will be
     * published in the {@code androidx.media3.common.Timeline} of the source as {@code
     * androidx.media3.common.Timeline.Window#tag}.
     *
     * <p>This method should only be called if {@link #setUri} is passed a non-null value.
     */
    public Builder setTag(@Nullable Object tag) {
      this.tag = tag;
      return this;
    }

    /** Sets the media metadata. */
    public Builder setMediaMetadata(MediaMetadata mediaMetadata) {
      this.mediaMetadata = mediaMetadata;
      return this;
    }

    /** Returns a new {@link MediaItem} instance with the current builder values. */
    @SuppressWarnings("deprecation") // Using PlaybackProperties while it exists.
    public MediaItem build() {
      // TODO: remove this check once all the deprecated individual DRM setters are removed.
      checkState(drmConfiguration.licenseUri == null || drmConfiguration.scheme != null);
      @Nullable PlaybackProperties localConfiguration = null;
      @Nullable Uri uri = this.uri;
      if (uri != null) {
        localConfiguration =
            new PlaybackProperties(
                uri,
                mimeType,
                drmConfiguration.scheme != null ? drmConfiguration.build() : null,
                adsConfiguration,
                streamKeys,
                customCacheKey,
                subtitleConfigurations,
                tag);
      }
      return new MediaItem(
          mediaId != null ? mediaId : DEFAULT_MEDIA_ID,
          clippingConfiguration.buildClippingProperties(),
          localConfiguration,
          liveConfiguration.build(),
          mediaMetadata != null ? mediaMetadata : MediaMetadata.EMPTY);
    }
  }

  /** DRM configuration for a media item. */
  public static final class DrmConfiguration {

    /** Builder for {@link DrmConfiguration}. */
    public static final class Builder {

      // TODO remove @Nullable annotation when the deprecated zero-arg constructor is removed.
      @Nullable private UUID scheme;
      @Nullable private Uri licenseUri;
      private ImmutableMap<String, String> licenseRequestHeaders;
      private boolean multiSession;
      private boolean playClearContentWithoutKey;
      private boolean forceDefaultLicenseUri;
      private ImmutableList<@C.TrackType Integer> forcedSessionTrackTypes;
      @Nullable private byte[] keySetId;

      /**
       * Constructs an instance.
       *
       * @param scheme The {@link UUID} of the protection scheme.
       */
      public Builder(UUID scheme) {
        this.scheme = scheme;
        this.licenseRequestHeaders = ImmutableMap.of();
        this.forcedSessionTrackTypes = ImmutableList.of();
      }

      /**
       * @deprecated This only exists to support the deprecated setters for individual DRM
       *     properties on {@link MediaItem.Builder}.
       */
      @Deprecated
      private Builder() {
        this.licenseRequestHeaders = ImmutableMap.of();
        this.forcedSessionTrackTypes = ImmutableList.of();
      }

      private Builder(DrmConfiguration drmConfiguration) {
        this.scheme = drmConfiguration.scheme;
        this.licenseUri = drmConfiguration.licenseUri;
        this.licenseRequestHeaders = drmConfiguration.licenseRequestHeaders;
        this.multiSession = drmConfiguration.multiSession;
        this.playClearContentWithoutKey = drmConfiguration.playClearContentWithoutKey;
        this.forceDefaultLicenseUri = drmConfiguration.forceDefaultLicenseUri;
        this.forcedSessionTrackTypes = drmConfiguration.forcedSessionTrackTypes;
        this.keySetId = drmConfiguration.keySetId;
      }

      /** Sets the {@link UUID} of the protection scheme. */
      public Builder setScheme(UUID scheme) {
        this.scheme = scheme;
        return this;
      }

      /**
       * @deprecated This only exists to support the deprecated {@link
       *     MediaItem.Builder#setDrmUuid(UUID)}.
       */
      @Deprecated
      private Builder setNullableScheme(@Nullable UUID scheme) {
        this.scheme = scheme;
        return this;
      }

      /** Sets the optional default DRM license server URI. */
      public Builder setLicenseUri(@Nullable Uri licenseUri) {
        this.licenseUri = licenseUri;
        return this;
      }

      /** Sets the optional default DRM license server URI. */
      public Builder setLicenseUri(@Nullable String licenseUri) {
        this.licenseUri = licenseUri == null ? null : Uri.parse(licenseUri);
        return this;
      }

      /** Sets the optional request headers attached to DRM license requests. */
      public Builder setLicenseRequestHeaders(Map<String, String> licenseRequestHeaders) {
        this.licenseRequestHeaders = ImmutableMap.copyOf(licenseRequestHeaders);
        return this;
      }

      /** Sets whether multi session is enabled. */
      public Builder setMultiSession(boolean multiSession) {
        this.multiSession = multiSession;
        return this;
      }

      /**
       * Sets whether to always use the default DRM license server URI even if the media specifies
       * its own DRM license server URI.
       */
      public Builder setForceDefaultLicenseUri(boolean forceDefaultLicenseUri) {
        this.forceDefaultLicenseUri = forceDefaultLicenseUri;
        return this;
      }

      /**
       * Sets whether clear samples within protected content should be played when keys for the
       * encrypted part of the content have yet to be loaded.
       */
      public Builder setPlayClearContentWithoutKey(boolean playClearContentWithoutKey) {
        this.playClearContentWithoutKey = playClearContentWithoutKey;
        return this;
      }

      /**
       * Sets whether a DRM session should be used for clear tracks of type {@link
       * C#TRACK_TYPE_VIDEO} and {@link C#TRACK_TYPE_AUDIO}.
       *
       * <p>This method overrides what has been set by previously calling {@link
       * #setForcedSessionTrackTypes(List)}.
       */
      public Builder forceSessionsForAudioAndVideoTracks(
          boolean useClearSessionsForAudioAndVideoTracks) {
        this.setForcedSessionTrackTypes(
            useClearSessionsForAudioAndVideoTracks
                ? ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO)
                : ImmutableList.of());
        return this;
      }

      /**
       * Sets a list of {@link C.TrackType track type} constants for which to use a DRM session even
       * when the tracks are in the clear.
       *
       * <p>For the common case of using a DRM session for {@link C#TRACK_TYPE_VIDEO} and {@link
       * C#TRACK_TYPE_AUDIO}, {@link #forceSessionsForAudioAndVideoTracks(boolean)} can be used.
       *
       * <p>This method overrides what has been set by previously calling {@link
       * #forceSessionsForAudioAndVideoTracks(boolean)}.
       */
      public Builder setForcedSessionTrackTypes(
          List<@C.TrackType Integer> forcedSessionTrackTypes) {
        this.forcedSessionTrackTypes = ImmutableList.copyOf(forcedSessionTrackTypes);
        return this;
      }

      /**
       * Sets the key set ID of the offline license.
       *
       * <p>The key set ID identifies an offline license. The ID is required to query, renew or
       * release an existing offline license (see {@code DefaultDrmSessionManager#setMode(int
       * mode,byte[] offlineLicenseKeySetId)}).
       */
      public Builder setKeySetId(@Nullable byte[] keySetId) {
        this.keySetId = keySetId != null ? Arrays.copyOf(keySetId, keySetId.length) : null;
        return this;
      }

      public DrmConfiguration build() {

        return new DrmConfiguration(this);
      }
    }

    /** The UUID of the protection scheme. */
    public final UUID scheme;

    /** @deprecated Use {@link #scheme} instead. */
    @UnstableApi @Deprecated public final UUID uuid;

    /**
     * Optional default DRM license server {@link Uri}. If {@code null} then the DRM license server
     * must be specified by the media.
     */
    @Nullable public final Uri licenseUri;

    /** @deprecated Use {@link #licenseRequestHeaders} instead. */
    @UnstableApi @Deprecated public final ImmutableMap<String, String> requestHeaders;

    /** The headers to attach to requests sent to the DRM license server. */
    public final ImmutableMap<String, String> licenseRequestHeaders;

    /** Whether the DRM configuration is multi session enabled. */
    public final boolean multiSession;

    /**
     * Whether clear samples within protected content should be played when keys for the encrypted
     * part of the content have yet to be loaded.
     */
    public final boolean playClearContentWithoutKey;

    /**
     * Whether to force use of {@link #licenseUri} even if the media specifies its own DRM license
     * server URI.
     */
    public final boolean forceDefaultLicenseUri;

    /** @deprecated Use {@link #forcedSessionTrackTypes}. */
    @UnstableApi @Deprecated public final ImmutableList<@C.TrackType Integer> sessionForClearTypes;
    /**
     * The types of tracks for which to always use a DRM session even if the content is unencrypted.
     */
    public final ImmutableList<@C.TrackType Integer> forcedSessionTrackTypes;

    @Nullable private final byte[] keySetId;

    @SuppressWarnings("deprecation") // Setting deprecated field
    private DrmConfiguration(Builder builder) {
      checkState(!(builder.forceDefaultLicenseUri && builder.licenseUri == null));
      this.scheme = checkNotNull(builder.scheme);
      this.uuid = scheme;
      this.licenseUri = builder.licenseUri;
      this.requestHeaders = builder.licenseRequestHeaders;
      this.licenseRequestHeaders = builder.licenseRequestHeaders;
      this.multiSession = builder.multiSession;
      this.forceDefaultLicenseUri = builder.forceDefaultLicenseUri;
      this.playClearContentWithoutKey = builder.playClearContentWithoutKey;
      this.sessionForClearTypes = builder.forcedSessionTrackTypes;
      this.forcedSessionTrackTypes = builder.forcedSessionTrackTypes;
      this.keySetId =
          builder.keySetId != null
              ? Arrays.copyOf(builder.keySetId, builder.keySetId.length)
              : null;
    }

    /** Returns the key set ID of the offline license. */
    @Nullable
    public byte[] getKeySetId() {
      return keySetId != null ? Arrays.copyOf(keySetId, keySetId.length) : null;
    }

    /** Returns a {@link Builder} initialized with the values of this instance. */
    public Builder buildUpon() {
      return new Builder(this);
    }

    @Override
    public boolean equals(@Nullable Object obj) {
      if (this == obj) {
        return true;
      }
      if (!(obj instanceof DrmConfiguration)) {
        return false;
      }

      DrmConfiguration other = (DrmConfiguration) obj;
      return scheme.equals(other.scheme)
          && Util.areEqual(licenseUri, other.licenseUri)
          && Util.areEqual(licenseRequestHeaders, other.licenseRequestHeaders)
          && multiSession == other.multiSession
          && forceDefaultLicenseUri == other.forceDefaultLicenseUri
          && playClearContentWithoutKey == other.playClearContentWithoutKey
          && forcedSessionTrackTypes.equals(other.forcedSessionTrackTypes)
          && Arrays.equals(keySetId, other.keySetId);
    }

    @Override
    public int hashCode() {
      int result = scheme.hashCode();
      result = 31 * result + (licenseUri != null ? licenseUri.hashCode() : 0);
      result = 31 * result + licenseRequestHeaders.hashCode();
      result = 31 * result + (multiSession ? 1 : 0);
      result = 31 * result + (forceDefaultLicenseUri ? 1 : 0);
      result = 31 * result + (playClearContentWithoutKey ? 1 : 0);
      result = 31 * result + forcedSessionTrackTypes.hashCode();
      result = 31 * result + Arrays.hashCode(keySetId);
      return result;
    }
  }

  /** Configuration for playing back linear ads with a media item. */
  public static final class AdsConfiguration {

    /** Builder for {@link AdsConfiguration} instances. */
    public static final class Builder {

      private Uri adTagUri;
      @Nullable private Object adsId;

      /**
       * Constructs a new instance.
       *
       * @param adTagUri The ad tag URI to load.
       */
      public Builder(Uri adTagUri) {
        this.adTagUri = adTagUri;
      }

      /** Sets the ad tag URI to load. */
      public Builder setAdTagUri(Uri adTagUri) {
        this.adTagUri = adTagUri;
        return this;
      }

      /**
       * Sets the ads identifier.
       *
       * <p>See details on {@link AdsConfiguration#adsId} for how the ads identifier is used and how
       * it's calculated if not explicitly set.
       */
      public Builder setAdsId(@Nullable Object adsId) {
        this.adsId = adsId;
        return this;
      }

      public AdsConfiguration build() {
        return new AdsConfiguration(this);
      }
    }

    /** The ad tag URI to load. */
    public final Uri adTagUri;

    /**
     * An opaque identifier for ad playback state associated with this item, or {@code null} if the
     * combination of the {@link MediaItem.Builder#setMediaId(String) media ID} and {@link #adTagUri
     * ad tag URI} should be used as the ads identifier.
     *
     * <p>Media items in the playlist that have the same ads identifier and ads loader share the
     * same ad playback state. To resume ad playback when recreating the playlist on returning from
     * the background, pass the same ads identifiers to the player.
     */
    @Nullable public final Object adsId;

    private AdsConfiguration(Builder builder) {
      this.adTagUri = builder.adTagUri;
      this.adsId = builder.adsId;
    }

    /** Returns a {@link Builder} initialized with the values of this instance. */
    public Builder buildUpon() {
      return new Builder(adTagUri).setAdsId(adsId);
    }

    @Override
    public boolean equals(@Nullable Object obj) {
      if (this == obj) {
        return true;
      }
      if (!(obj instanceof AdsConfiguration)) {
        return false;
      }

      AdsConfiguration other = (AdsConfiguration) obj;
      return adTagUri.equals(other.adTagUri) && Util.areEqual(adsId, other.adsId);
    }

    @Override
    public int hashCode() {
      int result = adTagUri.hashCode();
      result = 31 * result + (adsId != null ? adsId.hashCode() : 0);
      return result;
    }
  }

  /** Properties for local playback. */
  // TODO: Mark this final when PlaybackProperties is deleted.
  public static class LocalConfiguration {

    /** The {@link Uri}. */
    public final Uri uri;

    /**
     * The optional MIME type of the item, or {@code null} if unspecified.
     *
     * <p>The MIME type can be used to disambiguate media items that have a URI which does not allow
     * to infer the actual media type.
     */
    @Nullable public final String mimeType;

    /** Optional {@link DrmConfiguration} for the media. */
    @Nullable public final DrmConfiguration drmConfiguration;

    /** Optional ads configuration. */
    @Nullable public final AdsConfiguration adsConfiguration;

    /** Optional stream keys by which the manifest is filtered. */
    @UnstableApi public final List<StreamKey> streamKeys;

    /** Optional custom cache key (only used for progressive streams). */
    @UnstableApi @Nullable public final String customCacheKey;

    /** Optional subtitles to be sideloaded. */
    public final ImmutableList<SubtitleConfiguration> subtitleConfigurations;
    /** @deprecated Use {@link #subtitleConfigurations} instead. */
    @UnstableApi @Deprecated public final List<Subtitle> subtitles;

    /**
     * Optional tag for custom attributes. The tag for the media source which will be published in
     * the {@code androidx.media3.common.Timeline} of the source as {@code
     * androidx.media3.common.Timeline.Window#tag}.
     */
    @Nullable public final Object tag;

    @SuppressWarnings("deprecation") // Setting deprecated subtitles field.
    private LocalConfiguration(
        Uri uri,
        @Nullable String mimeType,
        @Nullable DrmConfiguration drmConfiguration,
        @Nullable AdsConfiguration adsConfiguration,
        List<StreamKey> streamKeys,
        @Nullable String customCacheKey,
        ImmutableList<SubtitleConfiguration> subtitleConfigurations,
        @Nullable Object tag) {
      this.uri = uri;
      this.mimeType = mimeType;
      this.drmConfiguration = drmConfiguration;
      this.adsConfiguration = adsConfiguration;
      this.streamKeys = streamKeys;
      this.customCacheKey = customCacheKey;
      this.subtitleConfigurations = subtitleConfigurations;
      ImmutableList.Builder<Subtitle> subtitles = ImmutableList.builder();
      for (int i = 0; i < subtitleConfigurations.size(); i++) {
        subtitles.add(subtitleConfigurations.get(i).buildUpon().buildSubtitle());
      }
      this.subtitles = subtitles.build();
      this.tag = tag;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
      if (this == obj) {
        return true;
      }
      if (!(obj instanceof LocalConfiguration)) {
        return false;
      }
      LocalConfiguration other = (LocalConfiguration) obj;

      return uri.equals(other.uri)
          && Util.areEqual(mimeType, other.mimeType)
          && Util.areEqual(drmConfiguration, other.drmConfiguration)
          && Util.areEqual(adsConfiguration, other.adsConfiguration)
          && streamKeys.equals(other.streamKeys)
          && Util.areEqual(customCacheKey, other.customCacheKey)
          && subtitleConfigurations.equals(other.subtitleConfigurations)
          && Util.areEqual(tag, other.tag);
    }

    @Override
    public int hashCode() {
      int result = uri.hashCode();
      result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode());
      result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode());
      result = 31 * result + (adsConfiguration == null ? 0 : adsConfiguration.hashCode());
      result = 31 * result + streamKeys.hashCode();
      result = 31 * result + (customCacheKey == null ? 0 : customCacheKey.hashCode());
      result = 31 * result + subtitleConfigurations.hashCode();
      result = 31 * result + (tag == null ? 0 : tag.hashCode());
      return result;
    }
  }

  /** @deprecated Use {@link LocalConfiguration}. */
  @UnstableApi
  @Deprecated
  public static final class PlaybackProperties extends LocalConfiguration {

    private PlaybackProperties(
        Uri uri,
        @Nullable String mimeType,
        @Nullable DrmConfiguration drmConfiguration,
        @Nullable AdsConfiguration adsConfiguration,
        List<StreamKey> streamKeys,
        @Nullable String customCacheKey,
        ImmutableList<SubtitleConfiguration> subtitleConfigurations,
        @Nullable Object tag) {
      super(
          uri,
          mimeType,
          drmConfiguration,
          adsConfiguration,
          streamKeys,
          customCacheKey,
          subtitleConfigurations,
          tag);
    }
  }

  /** Live playback configuration. */
  public static final class LiveConfiguration implements Bundleable {

    /** Builder for {@link LiveConfiguration} instances. */
    public static final class Builder {
      private long targetOffsetMs;
      private long minOffsetMs;
      private long maxOffsetMs;
      private float minPlaybackSpeed;
      private float maxPlaybackSpeed;

      /** Constructs an instance. */
      public Builder() {
        this.targetOffsetMs = C.TIME_UNSET;
        this.minOffsetMs = C.TIME_UNSET;
        this.maxOffsetMs = C.TIME_UNSET;
        this.minPlaybackSpeed = C.RATE_UNSET;
        this.maxPlaybackSpeed = C.RATE_UNSET;
      }

      private Builder(LiveConfiguration liveConfiguration) {
        this.targetOffsetMs = liveConfiguration.targetOffsetMs;
        this.minOffsetMs = liveConfiguration.minOffsetMs;
        this.maxOffsetMs = liveConfiguration.maxOffsetMs;
        this.minPlaybackSpeed = liveConfiguration.minPlaybackSpeed;
        this.maxPlaybackSpeed = liveConfiguration.maxPlaybackSpeed;
      }

      /**
       * Sets the target live offset, in milliseconds.
       *
       * <p>See {@code Player#getCurrentLiveOffset()}.
       *
       * <p>Defaults to {@link C#TIME_UNSET}, indicating the media-defined default will be used.
       */
      public Builder setTargetOffsetMs(long targetOffsetMs) {
        this.targetOffsetMs = targetOffsetMs;
        return this;
      }

      /**
       * Sets the minimum allowed live offset, in milliseconds.
       *
       * <p>See {@code Player#getCurrentLiveOffset()}.
       *
       * <p>Defaults to {@link C#TIME_UNSET}, indicating the media-defined default will be used.
       */
      public Builder setMinOffsetMs(long minOffsetMs) {
        this.minOffsetMs = minOffsetMs;
        return this;
      }

      /**
       * Sets the maximum allowed live offset, in milliseconds.
       *
       * <p>See {@code Player#getCurrentLiveOffset()}.
       *
       * <p>Defaults to {@link C#TIME_UNSET}, indicating the media-defined default will be used.
       */
      public Builder setMaxOffsetMs(long maxOffsetMs) {
        this.maxOffsetMs = maxOffsetMs;
        return this;
      }

      /**
       * Sets the minimum playback speed.
       *
       * <p>Defaults to {@link C#RATE_UNSET}, indicating the media-defined default will be used.
       */
      public Builder setMinPlaybackSpeed(float minPlaybackSpeed) {
        this.minPlaybackSpeed = minPlaybackSpeed;
        return this;
      }

      /**
       * Sets the maximum playback speed.
       *
       * <p>Defaults to {@link C#RATE_UNSET}, indicating the media-defined default will be used.
       */
      public Builder setMaxPlaybackSpeed(float maxPlaybackSpeed) {
        this.maxPlaybackSpeed = maxPlaybackSpeed;
        return this;
      }

      /** Creates a {@link LiveConfiguration} with the values from this builder. */
      public LiveConfiguration build() {
        return new LiveConfiguration(this);
      }
    }

    /**
     * A live playback configuration with unset values, meaning media-defined default values will be
     * used.
     */
    public static final LiveConfiguration UNSET = new LiveConfiguration.Builder().build();

    /**
     * Target offset from the live edge, in milliseconds, or {@link C#TIME_UNSET} to use the
     * media-defined default.
     */
    public final long targetOffsetMs;

    /**
     * The minimum allowed offset from the live edge, in milliseconds, or {@link C#TIME_UNSET} to
     * use the media-defined default.
     */
    public final long minOffsetMs;

    /**
     * The maximum allowed offset from the live edge, in milliseconds, or {@link C#TIME_UNSET} to
     * use the media-defined default.
     */
    public final long maxOffsetMs;

    /**
     * Minimum factor by which playback can be sped up, or {@link C#RATE_UNSET} to use the
     * media-defined default.
     */
    public final float minPlaybackSpeed;

    /**
     * Maximum factor by which playback can be sped up, or {@link C#RATE_UNSET} to use the
     * media-defined default.
     */
    public final float maxPlaybackSpeed;

    @SuppressWarnings("deprecation") // Using the deprecated constructor while it exists.
    private LiveConfiguration(Builder builder) {
      this(
          builder.targetOffsetMs,
          builder.minOffsetMs,
          builder.maxOffsetMs,
          builder.minPlaybackSpeed,
          builder.maxPlaybackSpeed);
    }

    /** @deprecated Use {@link Builder} instead. */
    @UnstableApi
    @Deprecated
    public LiveConfiguration(
        long targetOffsetMs,
        long minOffsetMs,
        long maxOffsetMs,
        float minPlaybackSpeed,
        float maxPlaybackSpeed) {
      this.targetOffsetMs = targetOffsetMs;
      this.minOffsetMs = minOffsetMs;
      this.maxOffsetMs = maxOffsetMs;
      this.minPlaybackSpeed = minPlaybackSpeed;
      this.maxPlaybackSpeed = maxPlaybackSpeed;
    }

    /** Returns a {@link Builder} initialized with the values of this instance. */
    public Builder buildUpon() {
      return new Builder(this);
    }

    @Override
    public boolean equals(@Nullable Object obj) {
      if (this == obj) {
        return true;
      }
      if (!(obj instanceof LiveConfiguration)) {
        return false;
      }
      LiveConfiguration other = (LiveConfiguration) obj;

      return targetOffsetMs == other.targetOffsetMs
          && minOffsetMs == other.minOffsetMs
          && maxOffsetMs == other.maxOffsetMs
          && minPlaybackSpeed == other.minPlaybackSpeed
          && maxPlaybackSpeed == other.maxPlaybackSpeed;
    }

    @Override
    public int hashCode() {
      int result = (int) (targetOffsetMs ^ (targetOffsetMs >>> 32));
      result = 31 * result + (int) (minOffsetMs ^ (minOffsetMs >>> 32));
      result = 31 * result + (int) (maxOffsetMs ^ (maxOffsetMs >>> 32));
      result = 31 * result + (minPlaybackSpeed != 0 ? Float.floatToIntBits(minPlaybackSpeed) : 0);
      result = 31 * result + (maxPlaybackSpeed != 0 ? Float.floatToIntBits(maxPlaybackSpeed) : 0);
      return result;
    }

    // Bundleable implementation.

    @Documented
    @Retention(RetentionPolicy.SOURCE)
    @Target(TYPE_USE)
    @IntDef({
      FIELD_TARGET_OFFSET_MS,
      FIELD_MIN_OFFSET_MS,
      FIELD_MAX_OFFSET_MS,
      FIELD_MIN_PLAYBACK_SPEED,
      FIELD_MAX_PLAYBACK_SPEED
    })
    private @interface FieldNumber {}

    private static final int FIELD_TARGET_OFFSET_MS = 0;
    private static final int FIELD_MIN_OFFSET_MS = 1;
    private static final int FIELD_MAX_OFFSET_MS = 2;
    private static final int FIELD_MIN_PLAYBACK_SPEED = 3;
    private static final int FIELD_MAX_PLAYBACK_SPEED = 4;

    @UnstableApi
    @Override
    public Bundle toBundle() {
      Bundle bundle = new Bundle();
      bundle.putLong(keyForField(FIELD_TARGET_OFFSET_MS), targetOffsetMs);
      bundle.putLong(keyForField(FIELD_MIN_OFFSET_MS), minOffsetMs);
      bundle.putLong(keyForField(FIELD_MAX_OFFSET_MS), maxOffsetMs);
      bundle.putFloat(keyForField(FIELD_MIN_PLAYBACK_SPEED), minPlaybackSpeed);
      bundle.putFloat(keyForField(FIELD_MAX_PLAYBACK_SPEED), maxPlaybackSpeed);
      return bundle;
    }

    /** Object that can restore {@link LiveConfiguration} from a {@link Bundle}. */
    @UnstableApi
    public static final Creator<LiveConfiguration> CREATOR =
        bundle ->
            new LiveConfiguration(
                bundle.getLong(
                    keyForField(FIELD_TARGET_OFFSET_MS), /* defaultValue= */ C.TIME_UNSET),
                bundle.getLong(keyForField(FIELD_MIN_OFFSET_MS), /* defaultValue= */ C.TIME_UNSET),
                bundle.getLong(keyForField(FIELD_MAX_OFFSET_MS), /* defaultValue= */ C.TIME_UNSET),
                bundle.getFloat(
                    keyForField(FIELD_MIN_PLAYBACK_SPEED), /* defaultValue= */ C.RATE_UNSET),
                bundle.getFloat(
                    keyForField(FIELD_MAX_PLAYBACK_SPEED), /* defaultValue= */ C.RATE_UNSET));

    private static String keyForField(@LiveConfiguration.FieldNumber int field) {
      return Integer.toString(field, Character.MAX_RADIX);
    }
  }

  /** Properties for a text track. */
  // TODO: Mark this final when Subtitle is deleted.
  public static class SubtitleConfiguration {

    /** Builder for {@link SubtitleConfiguration} instances. */
    public static final class Builder {
      private Uri uri;
      @Nullable private String mimeType;
      @Nullable private String language;
      private @C.SelectionFlags int selectionFlags;
      private @C.RoleFlags int roleFlags;
      @Nullable private String label;
      @Nullable private String id;

      /**
       * Constructs an instance.
       *
       * @param uri The {@link Uri} to the subtitle file.
       */
      public Builder(Uri uri) {
        this.uri = uri;
      }

      private Builder(SubtitleConfiguration subtitleConfiguration) {
        this.uri = subtitleConfiguration.uri;
        this.mimeType = subtitleConfiguration.mimeType;
        this.language = subtitleConfiguration.language;
        this.selectionFlags = subtitleConfiguration.selectionFlags;
        this.roleFlags = subtitleConfiguration.roleFlags;
        this.label = subtitleConfiguration.label;
        this.id = subtitleConfiguration.id;
      }

      /** Sets the {@link Uri} to the subtitle file. */
      public Builder setUri(Uri uri) {
        this.uri = uri;
        return this;
      }

      /** Sets the MIME type. */
      public Builder setMimeType(String mimeType) {
        this.mimeType = mimeType;
        return this;
      }

      /** Sets the optional language of the subtitle file. */
      public Builder setLanguage(@Nullable String language) {
        this.language = language;
        return this;
      }

      /** Sets the flags used for track selection. */
      public Builder setSelectionFlags(@C.SelectionFlags int selectionFlags) {
        this.selectionFlags = selectionFlags;
        return this;
      }

      /** Sets the role flags. These are used for track selection. */
      public Builder setRoleFlags(@C.RoleFlags int roleFlags) {
        this.roleFlags = roleFlags;
        return this;
      }

      /** Sets the optional label for this subtitle track. */
      public Builder setLabel(@Nullable String label) {
        this.label = label;
        return this;
      }

      /** Sets the optional ID for this subtitle track. */
      public Builder setId(@Nullable String id) {
        this.id = id;
        return this;
      }

      /** Creates a {@link SubtitleConfiguration} from the values of this builder. */
      public SubtitleConfiguration build() {
        return new SubtitleConfiguration(this);
      }

      private Subtitle buildSubtitle() {
        return new Subtitle(this);
      }
    }

    /** The {@link Uri} to the subtitle file. */
    public final Uri uri;
    /** The optional MIME type of the subtitle file, or {@code null} if unspecified. */
    @Nullable public final String mimeType;
    /** The language. */
    @Nullable public final String language;
    /** The selection flags. */
    public final @C.SelectionFlags int selectionFlags;
    /** The role flags. */
    public final @C.RoleFlags int roleFlags;
    /** The label. */
    @Nullable public final String label;
    /**
     * The ID of the subtitles. This will be propagated to the {@link Format#id} of the subtitle
     * track created from this configuration.
     */
    @Nullable public final String id;

    private SubtitleConfiguration(
        Uri uri,
        String mimeType,
        @Nullable String language,
        int selectionFlags,
        int roleFlags,
        @Nullable String label,
        @Nullable String id) {
      this.uri = uri;
      this.mimeType = mimeType;
      this.language = language;
      this.selectionFlags = selectionFlags;
      this.roleFlags = roleFlags;
      this.label = label;
      this.id = id;
    }

    private SubtitleConfiguration(Builder builder) {
      this.uri = builder.uri;
      this.mimeType = builder.mimeType;
      this.language = builder.language;
      this.selectionFlags = builder.selectionFlags;
      this.roleFlags = builder.roleFlags;
      this.label = builder.label;
      this.id = builder.id;
    }

    /** Returns a {@link Builder} initialized with the values of this instance. */
    public Builder buildUpon() {
      return new Builder(this);
    }

    @Override
    public boolean equals(@Nullable Object obj) {
      if (this == obj) {
        return true;
      }
      if (!(obj instanceof SubtitleConfiguration)) {
        return false;
      }

      SubtitleConfiguration other = (SubtitleConfiguration) obj;

      return uri.equals(other.uri)
          && Util.areEqual(mimeType, other.mimeType)
          && Util.areEqual(language, other.language)
          && selectionFlags == other.selectionFlags
          && roleFlags == other.roleFlags
          && Util.areEqual(label, other.label)
          && Util.areEqual(id, other.id);
    }

    @Override
    public int hashCode() {
      int result = uri.hashCode();
      result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode());
      result = 31 * result + (language == null ? 0 : language.hashCode());
      result = 31 * result + selectionFlags;
      result = 31 * result + roleFlags;
      result = 31 * result + (label == null ? 0 : label.hashCode());
      result = 31 * result + (id == null ? 0 : id.hashCode());
      return result;
    }
  }

  /** @deprecated Use {@link MediaItem.SubtitleConfiguration} instead */
  @UnstableApi
  @Deprecated
  public static final class Subtitle extends SubtitleConfiguration {

    /** @deprecated Use {@link Builder} instead. */
    @UnstableApi
    @Deprecated
    public Subtitle(Uri uri, String mimeType, @Nullable String language) {
      this(uri, mimeType, language, /* selectionFlags= */ 0);
    }

    /** @deprecated Use {@link Builder} instead. */
    @UnstableApi
    @Deprecated
    public Subtitle(
        Uri uri, String mimeType, @Nullable String language, @C.SelectionFlags int selectionFlags) {
      this(uri, mimeType, language, selectionFlags, /* roleFlags= */ 0, /* label= */ null);
    }

    /** @deprecated Use {@link Builder} instead. */
    @UnstableApi
    @Deprecated
    public Subtitle(
        Uri uri,
        String mimeType,
        @Nullable String language,
        @C.SelectionFlags int selectionFlags,
        @C.RoleFlags int roleFlags,
        @Nullable String label) {
      super(uri, mimeType, language, selectionFlags, roleFlags, label, /* id= */ null);
    }

    private Subtitle(Builder builder) {
      super(builder);
    }
  }

  /** Optionally clips the media item to a custom start and end position. */
  // TODO: Mark this final when ClippingProperties is deleted.
  public static class ClippingConfiguration implements Bundleable {

    /** A clipping configuration with default values. */
    public static final ClippingConfiguration UNSET = new ClippingConfiguration.Builder().build();

    /** Builder for {@link ClippingConfiguration} instances. */
    public static final class Builder {
      private long startPositionMs;
      private long endPositionMs;
      private boolean relativeToLiveWindow;
      private boolean relativeToDefaultPosition;
      private boolean startsAtKeyFrame;

      /** Constructs an instance. */
      public Builder() {
        endPositionMs = C.TIME_END_OF_SOURCE;
      }

      private Builder(ClippingConfiguration clippingConfiguration) {
        startPositionMs = clippingConfiguration.startPositionMs;
        endPositionMs = clippingConfiguration.endPositionMs;
        relativeToLiveWindow = clippingConfiguration.relativeToLiveWindow;
        relativeToDefaultPosition = clippingConfiguration.relativeToDefaultPosition;
        startsAtKeyFrame = clippingConfiguration.startsAtKeyFrame;
      }

      /**
       * Sets the optional start position in milliseconds which must be a value larger than or equal
       * to zero (Default: 0).
       */
      public Builder setStartPositionMs(@IntRange(from = 0) long startPositionMs) {
        Assertions.checkArgument(startPositionMs >= 0);
        this.startPositionMs = startPositionMs;
        return this;
      }

      /**
       * Sets the optional end position in milliseconds which must be a value larger than or equal
       * to zero, or {@link C#TIME_END_OF_SOURCE} to end when playback reaches the end of media
       * (Default: {@link C#TIME_END_OF_SOURCE}).
       */
      public Builder setEndPositionMs(long endPositionMs) {
        Assertions.checkArgument(endPositionMs == C.TIME_END_OF_SOURCE || endPositionMs >= 0);
        this.endPositionMs = endPositionMs;
        return this;
      }

      /**
       * Sets whether the start/end positions should move with the live window for live streams. If
       * {@code false}, live streams end when playback reaches the end position in live window seen
       * when the media is first loaded (Default: {@code false}).
       */
      public Builder setRelativeToLiveWindow(boolean relativeToLiveWindow) {
        this.relativeToLiveWindow = relativeToLiveWindow;
        return this;
      }

      /**
       * Sets whether the start position and the end position are relative to the default position
       * in the window (Default: {@code false}).
       */
      public Builder setRelativeToDefaultPosition(boolean relativeToDefaultPosition) {
        this.relativeToDefaultPosition = relativeToDefaultPosition;
        return this;
      }

      /**
       * Sets whether the start point is guaranteed to be a key frame. If {@code false}, the
       * playback transition into the clip may not be seamless (Default: {@code false}).
       */
      public Builder setStartsAtKeyFrame(boolean startsAtKeyFrame) {
        this.startsAtKeyFrame = startsAtKeyFrame;
        return this;
      }

      /**
       * Returns a {@link ClippingConfiguration} instance initialized with the values of this
       * builder.
       */
      public ClippingConfiguration build() {
        return buildClippingProperties();
      }

      /** @deprecated Use {@link #build()} instead. */
      @UnstableApi
      @Deprecated
      public ClippingProperties buildClippingProperties() {
        return new ClippingProperties(this);
      }
    }

    /** The start position in milliseconds. This is a value larger than or equal to zero. */
    @IntRange(from = 0)
    public final long startPositionMs;

    /**
     * The end position in milliseconds. This is a value larger than or equal to zero or {@link
     * C#TIME_END_OF_SOURCE} to play to the end of the stream.
     */
    public final long endPositionMs;

    /**
     * Whether the clipping of active media periods moves with a live window. If {@code false},
     * playback ends when it reaches {@link #endPositionMs}.
     */
    public final boolean relativeToLiveWindow;

    /**
     * Whether {@link #startPositionMs} and {@link #endPositionMs} are relative to the default
     * position.
     */
    public final boolean relativeToDefaultPosition;

    /** Sets whether the start point is guaranteed to be a key frame. */
    public final boolean startsAtKeyFrame;

    private ClippingConfiguration(Builder builder) {
      this.startPositionMs = builder.startPositionMs;
      this.endPositionMs = builder.endPositionMs;
      this.relativeToLiveWindow = builder.relativeToLiveWindow;
      this.relativeToDefaultPosition = builder.relativeToDefaultPosition;
      this.startsAtKeyFrame = builder.startsAtKeyFrame;
    }

    /** Returns a {@link Builder} initialized with the values of this instance. */
    public Builder buildUpon() {
      return new Builder(this);
    }

    @Override
    public boolean equals(@Nullable Object obj) {
      if (this == obj) {
        return true;
      }
      if (!(obj instanceof ClippingConfiguration)) {
        return false;
      }

      ClippingConfiguration other = (ClippingConfiguration) obj;

      return startPositionMs == other.startPositionMs
          && endPositionMs == other.endPositionMs
          && relativeToLiveWindow == other.relativeToLiveWindow
          && relativeToDefaultPosition == other.relativeToDefaultPosition
          && startsAtKeyFrame == other.startsAtKeyFrame;
    }

    @Override
    public int hashCode() {
      int result = (int) (startPositionMs ^ (startPositionMs >>> 32));
      result = 31 * result + (int) (endPositionMs ^ (endPositionMs >>> 32));
      result = 31 * result + (relativeToLiveWindow ? 1 : 0);
      result = 31 * result + (relativeToDefaultPosition ? 1 : 0);
      result = 31 * result + (startsAtKeyFrame ? 1 : 0);
      return result;
    }

    // Bundleable implementation.

    @Documented
    @Retention(RetentionPolicy.SOURCE)
    @Target(TYPE_USE)
    @IntDef({
      FIELD_START_POSITION_MS,
      FIELD_END_POSITION_MS,
      FIELD_RELATIVE_TO_LIVE_WINDOW,
      FIELD_RELATIVE_TO_DEFAULT_POSITION,
      FIELD_STARTS_AT_KEY_FRAME
    })
    private @interface FieldNumber {}

    private static final int FIELD_START_POSITION_MS = 0;
    private static final int FIELD_END_POSITION_MS = 1;
    private static final int FIELD_RELATIVE_TO_LIVE_WINDOW = 2;
    private static final int FIELD_RELATIVE_TO_DEFAULT_POSITION = 3;
    private static final int FIELD_STARTS_AT_KEY_FRAME = 4;

    @UnstableApi
    @Override
    public Bundle toBundle() {
      Bundle bundle = new Bundle();
      bundle.putLong(keyForField(FIELD_START_POSITION_MS), startPositionMs);
      bundle.putLong(keyForField(FIELD_END_POSITION_MS), endPositionMs);
      bundle.putBoolean(keyForField(FIELD_RELATIVE_TO_LIVE_WINDOW), relativeToLiveWindow);
      bundle.putBoolean(keyForField(FIELD_RELATIVE_TO_DEFAULT_POSITION), relativeToDefaultPosition);
      bundle.putBoolean(keyForField(FIELD_STARTS_AT_KEY_FRAME), startsAtKeyFrame);
      return bundle;
    }

    /** Object that can restore {@link ClippingConfiguration} from a {@link Bundle}. */
    @UnstableApi
    public static final Creator<ClippingProperties> CREATOR =
        bundle ->
            new ClippingConfiguration.Builder()
                .setStartPositionMs(
                    bundle.getLong(keyForField(FIELD_START_POSITION_MS), /* defaultValue= */ 0))
                .setEndPositionMs(
                    bundle.getLong(
                        keyForField(FIELD_END_POSITION_MS),
                        /* defaultValue= */ C.TIME_END_OF_SOURCE))
                .setRelativeToLiveWindow(
                    bundle.getBoolean(keyForField(FIELD_RELATIVE_TO_LIVE_WINDOW), false))
                .setRelativeToDefaultPosition(
                    bundle.getBoolean(keyForField(FIELD_RELATIVE_TO_DEFAULT_POSITION), false))
                .setStartsAtKeyFrame(
                    bundle.getBoolean(keyForField(FIELD_STARTS_AT_KEY_FRAME), false))
                .buildClippingProperties();

    private static String keyForField(@ClippingConfiguration.FieldNumber int field) {
      return Integer.toString(field, Character.MAX_RADIX);
    }
  }

  /** @deprecated Use {@link ClippingConfiguration} instead. */
  @UnstableApi
  @Deprecated
  public static final class ClippingProperties extends ClippingConfiguration {
    public static final ClippingProperties UNSET =
        new ClippingConfiguration.Builder().buildClippingProperties();

    private ClippingProperties(Builder builder) {
      super(builder);
    }
  }

  /**
   * The default media ID that is used if the media ID is not explicitly set by {@link
   * Builder#setMediaId(String)}.
   */
  public static final String DEFAULT_MEDIA_ID = "";

  /** Empty {@link MediaItem}. */
  public static final MediaItem EMPTY = new MediaItem.Builder().build();

  /** Identifies the media item. */
  public final String mediaId;

  /**
   * Optional configuration for local playback. May be {@code null} if shared over process
   * boundaries.
   */
  @Nullable public final LocalConfiguration localConfiguration;
  /** @deprecated Use {@link #localConfiguration} instead. */
  @UnstableApi @Deprecated @Nullable public final PlaybackProperties playbackProperties;

  /** The live playback configuration. */
  public final LiveConfiguration liveConfiguration;

  /** The media metadata. */
  public final MediaMetadata mediaMetadata;

  /** The clipping properties. */
  public final ClippingConfiguration clippingConfiguration;
  /** @deprecated Use {@link #clippingConfiguration} instead. */
  @UnstableApi @Deprecated public final ClippingProperties clippingProperties;

  // Using PlaybackProperties and ClippingProperties until they're deleted.
  @SuppressWarnings("deprecation")
  private MediaItem(
      String mediaId,
      ClippingProperties clippingConfiguration,
      @Nullable PlaybackProperties localConfiguration,
      LiveConfiguration liveConfiguration,
      MediaMetadata mediaMetadata) {
    this.mediaId = mediaId;
    this.localConfiguration = localConfiguration;
    this.playbackProperties = localConfiguration;
    this.liveConfiguration = liveConfiguration;
    this.mediaMetadata = mediaMetadata;
    this.clippingConfiguration = clippingConfiguration;
    this.clippingProperties = clippingConfiguration;
  }

  /** Returns a {@link Builder} initialized with the values of this instance. */
  public Builder buildUpon() {
    return new Builder(this);
  }

  @Override
  public boolean equals(@Nullable Object obj) {
    if (this == obj) {
      return true;
    }
    if (!(obj instanceof MediaItem)) {
      return false;
    }

    MediaItem other = (MediaItem) obj;

    return Util.areEqual(mediaId, other.mediaId)
        && clippingConfiguration.equals(other.clippingConfiguration)
        && Util.areEqual(localConfiguration, other.localConfiguration)
        && Util.areEqual(liveConfiguration, other.liveConfiguration)
        && Util.areEqual(mediaMetadata, other.mediaMetadata);
  }

  @Override
  public int hashCode() {
    int result = mediaId.hashCode();
    result = 31 * result + (localConfiguration != null ? localConfiguration.hashCode() : 0);
    result = 31 * result + liveConfiguration.hashCode();
    result = 31 * result + clippingConfiguration.hashCode();
    result = 31 * result + mediaMetadata.hashCode();
    return result;
  }

  // Bundleable implementation.

  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @Target(TYPE_USE)
  @IntDef({
    FIELD_MEDIA_ID,
    FIELD_LIVE_CONFIGURATION,
    FIELD_MEDIA_METADATA,
    FIELD_CLIPPING_PROPERTIES
  })
  private @interface FieldNumber {}

  private static final int FIELD_MEDIA_ID = 0;
  private static final int FIELD_LIVE_CONFIGURATION = 1;
  private static final int FIELD_MEDIA_METADATA = 2;
  private static final int FIELD_CLIPPING_PROPERTIES = 3;

  /**
   * {@inheritDoc}
   *
   * <p>It omits the {@link #localConfiguration} field. The {@link #localConfiguration} of an
   * instance restored by {@link #CREATOR} will always be {@code null}.
   */
  @UnstableApi
  @Override
  public Bundle toBundle() {
    Bundle bundle = new Bundle();
    bundle.putString(keyForField(FIELD_MEDIA_ID), mediaId);
    bundle.putBundle(keyForField(FIELD_LIVE_CONFIGURATION), liveConfiguration.toBundle());
    bundle.putBundle(keyForField(FIELD_MEDIA_METADATA), mediaMetadata.toBundle());
    bundle.putBundle(keyForField(FIELD_CLIPPING_PROPERTIES), clippingConfiguration.toBundle());
    return bundle;
  }

  /**
   * Object that can restore {@link MediaItem} from a {@link Bundle}.
   *
   * <p>The {@link #localConfiguration} of a restored instance will always be {@code null}.
   */
  @UnstableApi public static final Creator<MediaItem> CREATOR = MediaItem::fromBundle;

  @SuppressWarnings("deprecation") // Unbundling to ClippingProperties while it still exists.
  private static MediaItem fromBundle(Bundle bundle) {
    String mediaId = checkNotNull(bundle.getString(keyForField(FIELD_MEDIA_ID), DEFAULT_MEDIA_ID));
    @Nullable
    Bundle liveConfigurationBundle = bundle.getBundle(keyForField(FIELD_LIVE_CONFIGURATION));
    LiveConfiguration liveConfiguration;
    if (liveConfigurationBundle == null) {
      liveConfiguration = LiveConfiguration.UNSET;
    } else {
      liveConfiguration = LiveConfiguration.CREATOR.fromBundle(liveConfigurationBundle);
    }
    @Nullable Bundle mediaMetadataBundle = bundle.getBundle(keyForField(FIELD_MEDIA_METADATA));
    MediaMetadata mediaMetadata;
    if (mediaMetadataBundle == null) {
      mediaMetadata = MediaMetadata.EMPTY;
    } else {
      mediaMetadata = MediaMetadata.CREATOR.fromBundle(mediaMetadataBundle);
    }
    @Nullable
    Bundle clippingConfigurationBundle = bundle.getBundle(keyForField(FIELD_CLIPPING_PROPERTIES));
    ClippingProperties clippingConfiguration;
    if (clippingConfigurationBundle == null) {
      clippingConfiguration = ClippingProperties.UNSET;
    } else {
      clippingConfiguration = ClippingConfiguration.CREATOR.fromBundle(clippingConfigurationBundle);
    }
    return new MediaItem(
        mediaId,
        clippingConfiguration,
        /* localConfiguration= */ null,
        liveConfiguration,
        mediaMetadata);
  }

  private static String keyForField(@FieldNumber int field) {
    return Integer.toString(field, Character.MAX_RADIX);
  }
}