Tracks.java

/*
 * Copyright (C) 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.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.BundleableUtil.toBundleArrayList;
import static java.lang.annotation.ElementType.TYPE_USE;

import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Booleans;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Arrays;
import java.util.List;

/** Information about groups of tracks. */
public final class Tracks implements Bundleable {

  /**
   * Information about a single group of tracks, including the underlying {@link TrackGroup}, the
   * level to which each track is supported by the player, and whether any of the tracks are
   * selected.
   */
  public static final class Group implements Bundleable {

    /** The number of tracks in the group. */
    public final int length;

    private final TrackGroup mediaTrackGroup;
    private final boolean adaptiveSupported;
    private final @C.FormatSupport int[] trackSupport;
    private final boolean[] trackSelected;

    /**
     * Constructs an instance.
     *
     * @param mediaTrackGroup The underlying {@link TrackGroup} defined by the media.
     * @param adaptiveSupported Whether the player supports adaptive selections containing more than
     *     one track in the group.
     * @param trackSupport The {@link C.FormatSupport} of each track in the group.
     * @param trackSelected Whether each track in the {@code trackGroup} is selected.
     */
    @UnstableApi
    public Group(
        TrackGroup mediaTrackGroup,
        boolean adaptiveSupported,
        @C.FormatSupport int[] trackSupport,
        boolean[] trackSelected) {
      length = mediaTrackGroup.length;
      checkArgument(length == trackSupport.length && length == trackSelected.length);
      this.mediaTrackGroup = mediaTrackGroup;
      this.adaptiveSupported = adaptiveSupported && length > 1;
      this.trackSupport = trackSupport.clone();
      this.trackSelected = trackSelected.clone();
    }

    /**
     * Returns the underlying {@link TrackGroup} defined by the media.
     *
     * <p>Unlike this class, {@link TrackGroup} only contains information defined by the media
     * itself, and does not contain runtime information such as which tracks are supported and
     * currently selected. This makes it suitable for use as a {@code key} in certain {@code (key,
     * value)} data structures.
     */
    public TrackGroup getMediaTrackGroup() {
      return mediaTrackGroup;
    }

    /**
     * Returns the {@link Format} for a specified track.
     *
     * @param trackIndex The index of the track in the group.
     * @return The {@link Format} of the track.
     */
    public Format getTrackFormat(int trackIndex) {
      return mediaTrackGroup.getFormat(trackIndex);
    }

    /**
     * Returns the level of support for a specified track.
     *
     * @param trackIndex The index of the track in the group.
     * @return The {@link C.FormatSupport} of the track.
     */
    @UnstableApi
    public @C.FormatSupport int getTrackSupport(int trackIndex) {
      return trackSupport[trackIndex];
    }

    /**
     * Returns whether a specified track is supported for playback, without exceeding the advertised
     * capabilities of the device. Equivalent to {@code isTrackSupported(trackIndex, false)}.
     *
     * @param trackIndex The index of the track in the group.
     * @return True if the track's format can be played, false otherwise.
     */
    public boolean isTrackSupported(int trackIndex) {
      return isTrackSupported(trackIndex, /* allowExceedsCapabilities= */ false);
    }

    /**
     * Returns whether a specified track is supported for playback.
     *
     * @param trackIndex The index of the track in the group.
     * @param allowExceedsCapabilities Whether to consider the track as supported if it has a
     *     supported {@link Format#sampleMimeType MIME type}, but otherwise exceeds the advertised
     *     capabilities of the device. For example, a video track for which there's a corresponding
     *     decoder whose maximum advertised resolution is exceeded by the resolution of the track.
     *     Such tracks may be playable in some cases.
     * @return True if the track's format can be played, false otherwise.
     */
    public boolean isTrackSupported(int trackIndex, boolean allowExceedsCapabilities) {
      return trackSupport[trackIndex] == C.FORMAT_HANDLED
          || (allowExceedsCapabilities
              && trackSupport[trackIndex] == C.FORMAT_EXCEEDS_CAPABILITIES);
    }

    /** Returns whether at least one track in the group is selected for playback. */
    public boolean isSelected() {
      return Booleans.contains(trackSelected, true);
    }

    /** Returns whether adaptive selections containing more than one track are supported. */
    public boolean isAdaptiveSupported() {
      return adaptiveSupported;
    }

    /**
     * Returns whether at least one track in the group is supported for playback, without exceeding
     * the advertised capabilities of the device. Equivalent to {@code isSupported(false)}.
     */
    public boolean isSupported() {
      return isSupported(/* allowExceedsCapabilities= */ false);
    }

    /**
     * Returns whether at least one track in the group is supported for playback.
     *
     * @param allowExceedsCapabilities Whether to consider a track as supported if it has a
     *     supported {@link Format#sampleMimeType MIME type}, but otherwise exceeds the advertised
     *     capabilities of the device. For example, a video track for which there's a corresponding
     *     decoder whose maximum advertised resolution is exceeded by the resolution of the track.
     *     Such tracks may be playable in some cases.
     */
    public boolean isSupported(boolean allowExceedsCapabilities) {
      for (int i = 0; i < trackSupport.length; i++) {
        if (isTrackSupported(i, allowExceedsCapabilities)) {
          return true;
        }
      }
      return false;
    }

    /**
     * Returns whether a specified track is selected for playback.
     *
     * <p>Note that multiple tracks in the group may be selected. This is common in adaptive
     * streaming, where tracks of different qualities are selected and the player switches between
     * them during playback (e.g., based on the available network bandwidth).
     *
     * <p>This class doesn't provide a way to determine which of the selected tracks is currently
     * playing, however some player implementations have ways of getting such information. For
     * example, ExoPlayer provides this information via {@code ExoTrackSelection.getSelectedFormat}.
     *
     * @param trackIndex The index of the track in the group.
     * @return True if the track is selected, false otherwise.
     */
    public boolean isTrackSelected(int trackIndex) {
      return trackSelected[trackIndex];
    }

    /** Returns the {@link C.TrackType} of the group. */
    public @C.TrackType int getType() {
      return mediaTrackGroup.type;
    }

    @Override
    public boolean equals(@Nullable Object other) {
      if (this == other) {
        return true;
      }
      if (other == null || getClass() != other.getClass()) {
        return false;
      }
      Group that = (Group) other;
      return adaptiveSupported == that.adaptiveSupported
          && mediaTrackGroup.equals(that.mediaTrackGroup)
          && Arrays.equals(trackSupport, that.trackSupport)
          && Arrays.equals(trackSelected, that.trackSelected);
    }

    @Override
    public int hashCode() {
      int result = mediaTrackGroup.hashCode();
      result = 31 * result + (adaptiveSupported ? 1 : 0);
      result = 31 * result + Arrays.hashCode(trackSupport);
      result = 31 * result + Arrays.hashCode(trackSelected);
      return result;
    }

    // Bundleable implementation.
    @Documented
    @Retention(RetentionPolicy.SOURCE)
    @Target(TYPE_USE)
    @IntDef({
      FIELD_TRACK_GROUP,
      FIELD_TRACK_SUPPORT,
      FIELD_TRACK_SELECTED,
      FIELD_ADAPTIVE_SUPPORTED,
    })
    private @interface FieldNumber {}

    private static final int FIELD_TRACK_GROUP = 0;
    private static final int FIELD_TRACK_SUPPORT = 1;
    private static final int FIELD_TRACK_SELECTED = 3;
    private static final int FIELD_ADAPTIVE_SUPPORTED = 4;

    @Override
    public Bundle toBundle() {
      Bundle bundle = new Bundle();
      bundle.putBundle(keyForField(FIELD_TRACK_GROUP), mediaTrackGroup.toBundle());
      bundle.putIntArray(keyForField(FIELD_TRACK_SUPPORT), trackSupport);
      bundle.putBooleanArray(keyForField(FIELD_TRACK_SELECTED), trackSelected);
      bundle.putBoolean(keyForField(FIELD_ADAPTIVE_SUPPORTED), adaptiveSupported);
      return bundle;
    }

    /** Object that can restore a group of tracks from a {@link Bundle}. */
    @UnstableApi
    public static final Creator<Group> CREATOR =
        bundle -> {
          // Can't create a Tracks.Group without a TrackGroup
          TrackGroup trackGroup =
              TrackGroup.CREATOR.fromBundle(
                  checkNotNull(bundle.getBundle(keyForField(FIELD_TRACK_GROUP))));
          final @C.FormatSupport int[] trackSupport =
              MoreObjects.firstNonNull(
                  bundle.getIntArray(keyForField(FIELD_TRACK_SUPPORT)), new int[trackGroup.length]);
          boolean[] selected =
              MoreObjects.firstNonNull(
                  bundle.getBooleanArray(keyForField(FIELD_TRACK_SELECTED)),
                  new boolean[trackGroup.length]);
          boolean adaptiveSupported =
              bundle.getBoolean(keyForField(FIELD_ADAPTIVE_SUPPORTED), false);
          return new Group(trackGroup, adaptiveSupported, trackSupport, selected);
        };

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

  /** Empty tracks. */
  public static final Tracks EMPTY = new Tracks(ImmutableList.of());

  private final ImmutableList<Group> groups;

  /**
   * Constructs an instance.
   *
   * @param groups The {@link Group groups} of tracks.
   */
  @UnstableApi
  public Tracks(List<Group> groups) {
    this.groups = ImmutableList.copyOf(groups);
  }

  /** Returns the {@link Group groups} of tracks. */
  public ImmutableList<Group> getGroups() {
    return groups;
  }

  /** Returns {@code true} if there are no tracks, and {@code false} otherwise. */
  public boolean isEmpty() {
    return groups.isEmpty();
  }

  /** Returns true if there are tracks of type {@code trackType}, and false otherwise. */
  public boolean containsType(@C.TrackType int trackType) {
    for (int i = 0; i < groups.size(); i++) {
      if (groups.get(i).getType() == trackType) {
        return true;
      }
    }
    return false;
  }

  /**
   * Returns true if at least one track of type {@code trackType} is {@link
   * Group#isTrackSupported(int) supported}.
   */
  public boolean isTypeSupported(@C.TrackType int trackType) {
    return isTypeSupported(trackType, /* allowExceedsCapabilities= */ false);
  }

  /**
   * Returns true if at least one track of type {@code trackType} is {@link
   * Group#isTrackSupported(int, boolean) supported}.
   *
   * @param allowExceedsCapabilities Whether to consider the track as supported if it has a
   *     supported {@link Format#sampleMimeType MIME type}, but otherwise exceeds the advertised
   *     capabilities of the device. For example, a video track for which there's a corresponding
   *     decoder whose maximum advertised resolution is exceeded by the resolution of the track.
   *     Such tracks may be playable in some cases.
   */
  public boolean isTypeSupported(@C.TrackType int trackType, boolean allowExceedsCapabilities) {
    for (int i = 0; i < groups.size(); i++) {
      if (groups.get(i).getType() == trackType) {
        if (groups.get(i).isSupported(allowExceedsCapabilities)) {
          return true;
        }
      }
    }
    return false;
  }

  /**
   * @deprecated Use {@link #containsType(int)} and {@link #isTypeSupported(int)}.
   */
  @Deprecated
  @UnstableApi
  @SuppressWarnings("deprecation")
  public boolean isTypeSupportedOrEmpty(@C.TrackType int trackType) {
    return isTypeSupportedOrEmpty(trackType, /* allowExceedsCapabilities= */ false);
  }

  /**
   * @deprecated Use {@link #containsType(int)} and {@link #isTypeSupported(int, boolean)}.
   */
  @Deprecated
  @UnstableApi
  public boolean isTypeSupportedOrEmpty(
      @C.TrackType int trackType, boolean allowExceedsCapabilities) {
    return !containsType(trackType) || isTypeSupported(trackType, allowExceedsCapabilities);
  }

  /** Returns true if at least one track of the type {@code trackType} is selected for playback. */
  public boolean isTypeSelected(@C.TrackType int trackType) {
    for (int i = 0; i < groups.size(); i++) {
      Group group = groups.get(i);
      if (group.isSelected() && group.getType() == trackType) {
        return true;
      }
    }
    return false;
  }

  @Override
  public boolean equals(@Nullable Object other) {
    if (this == other) {
      return true;
    }
    if (other == null || getClass() != other.getClass()) {
      return false;
    }
    Tracks that = (Tracks) other;
    return groups.equals(that.groups);
  }

  @Override
  public int hashCode() {
    return groups.hashCode();
  }
  // Bundleable implementation.

  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @Target(TYPE_USE)
  @IntDef({
    FIELD_TRACK_GROUPS,
  })
  private @interface FieldNumber {}

  private static final int FIELD_TRACK_GROUPS = 0;

  @UnstableApi
  @Override
  public Bundle toBundle() {
    Bundle bundle = new Bundle();
    bundle.putParcelableArrayList(keyForField(FIELD_TRACK_GROUPS), toBundleArrayList(groups));
    return bundle;
  }

  /** Object that can restore tracks from a {@link Bundle}. */
  @UnstableApi
  public static final Creator<Tracks> CREATOR =
      bundle -> {
        @Nullable
        List<Bundle> groupBundles = bundle.getParcelableArrayList(keyForField(FIELD_TRACK_GROUPS));
        List<Group> groups =
            groupBundles == null
                ? ImmutableList.of()
                : BundleableUtil.fromBundleList(Group.CREATOR, groupBundles);
        return new Tracks(groups);
      };

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