CastTimeline.java

/*
 * Copyright (C) 2017 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.cast;

import android.util.SparseArray;
import android.util.SparseIntArray;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import com.google.android.gms.cast.MediaInfo;
import java.util.Arrays;

/** A {@link Timeline} for Cast media queues. */
/* package */ final class CastTimeline extends Timeline {

  /** Holds {@link Timeline} related data for a Cast media item. */
  public static final class ItemData {

    /* package */ static final String UNKNOWN_CONTENT_ID = "UNKNOWN_CONTENT_ID";

    /** Holds no media information. */
    public static final ItemData EMPTY =
        new ItemData(
            /* durationUs= */ C.TIME_UNSET,
            /* defaultPositionUs= */ C.TIME_UNSET,
            /* isLive= */ false,
            MediaItem.EMPTY,
            UNKNOWN_CONTENT_ID);

    /** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */
    public final long durationUs;
    /**
     * The default start position of the item in microseconds, or {@link C#TIME_UNSET} if unknown.
     */
    public final long defaultPositionUs;
    /** Whether the item is live content, or {@code false} if unknown. */
    public final boolean isLive;
    /** The original media item that has been set or added to the playlist. */
    public final MediaItem mediaItem;
    /** The {@linkplain MediaInfo#getContentId() content ID} of the cast media queue item. */
    public final String contentId;

    /**
     * Creates an instance.
     *
     * @param durationUs See {@link #durationsUs}.
     * @param defaultPositionUs See {@link #defaultPositionUs}.
     * @param isLive See {@link #isLive}.
     * @param mediaItem See {@link #mediaItem}.
     * @param contentId See {@link #contentId}.
     */
    public ItemData(
        long durationUs,
        long defaultPositionUs,
        boolean isLive,
        MediaItem mediaItem,
        String contentId) {
      this.durationUs = durationUs;
      this.defaultPositionUs = defaultPositionUs;
      this.isLive = isLive;
      this.mediaItem = mediaItem;
      this.contentId = contentId;
    }

    /**
     * Returns a copy of this instance with the given values.
     *
     * @param durationUs The duration in microseconds, or {@link C#TIME_UNSET} if unknown.
     * @param defaultPositionUs The default start position in microseconds, or {@link C#TIME_UNSET}
     *     if unknown.
     * @param isLive Whether the item is live, or {@code false} if unknown.
     * @param mediaItem The media item.
     * @param contentId The content ID.
     */
    public ItemData copyWithNewValues(
        long durationUs,
        long defaultPositionUs,
        boolean isLive,
        MediaItem mediaItem,
        String contentId) {
      if (durationUs == this.durationUs
          && defaultPositionUs == this.defaultPositionUs
          && isLive == this.isLive
          && contentId.equals(this.contentId)
          && mediaItem.equals(this.mediaItem)) {
        return this;
      }
      return new ItemData(durationUs, defaultPositionUs, isLive, mediaItem, contentId);
    }
  }

  /** {@link Timeline} for a cast queue that has no items. */
  public static final CastTimeline EMPTY_CAST_TIMELINE =
      new CastTimeline(new int[0], new SparseArray<>());

  private final SparseIntArray idsToIndex;
  private final MediaItem[] mediaItems;
  private final int[] ids;
  private final long[] durationsUs;
  private final long[] defaultPositionsUs;
  private final boolean[] isLive;

  /**
   * Creates a Cast timeline from the given data.
   *
   * @param itemIds The ids of the items in the timeline.
   * @param itemIdToData Maps item ids to {@link ItemData}.
   */
  public CastTimeline(int[] itemIds, SparseArray<ItemData> itemIdToData) {
    int itemCount = itemIds.length;
    idsToIndex = new SparseIntArray(itemCount);
    ids = Arrays.copyOf(itemIds, itemCount);
    durationsUs = new long[itemCount];
    defaultPositionsUs = new long[itemCount];
    isLive = new boolean[itemCount];
    mediaItems = new MediaItem[itemCount];
    for (int i = 0; i < ids.length; i++) {
      int id = ids[i];
      idsToIndex.put(id, i);
      ItemData data = itemIdToData.get(id, ItemData.EMPTY);
      mediaItems[i] = data.mediaItem;
      durationsUs[i] = data.durationUs;
      defaultPositionsUs[i] = data.defaultPositionUs == C.TIME_UNSET ? 0 : data.defaultPositionUs;
      isLive[i] = data.isLive;
    }
  }

  // Timeline implementation.

  @Override
  public int getWindowCount() {
    return ids.length;
  }

  @Override
  public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
    long durationUs = durationsUs[windowIndex];
    boolean isDynamic = durationUs == C.TIME_UNSET;
    return window.set(
        /* uid= */ ids[windowIndex],
        /* mediaItem= */ mediaItems[windowIndex],
        /* manifest= */ null,
        /* presentationStartTimeMs= */ C.TIME_UNSET,
        /* windowStartTimeMs= */ C.TIME_UNSET,
        /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
        /* isSeekable= */ !isDynamic,
        isDynamic,
        isLive[windowIndex] ? mediaItems[windowIndex].liveConfiguration : null,
        defaultPositionsUs[windowIndex],
        durationUs,
        /* firstPeriodIndex= */ windowIndex,
        /* lastPeriodIndex= */ windowIndex,
        /* positionInFirstPeriodUs= */ 0);
  }

  @Override
  public int getPeriodCount() {
    return ids.length;
  }

  @Override
  public Period getPeriod(int periodIndex, Period period, boolean setIds) {
    int id = ids[periodIndex];
    return period.set(id, id, periodIndex, durationsUs[periodIndex], 0);
  }

  @Override
  public int getIndexOfPeriod(Object uid) {
    return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET;
  }

  @Override
  public Integer getUidOfPeriod(int periodIndex) {
    return ids[periodIndex];
  }

  // equals and hashCode implementations.

  @Override
  public boolean equals(@Nullable Object other) {
    if (this == other) {
      return true;
    } else if (!(other instanceof CastTimeline)) {
      return false;
    }
    CastTimeline that = (CastTimeline) other;
    return Arrays.equals(ids, that.ids)
        && Arrays.equals(durationsUs, that.durationsUs)
        && Arrays.equals(defaultPositionsUs, that.defaultPositionsUs)
        && Arrays.equals(isLive, that.isLive);
  }

  @Override
  public int hashCode() {
    int result = Arrays.hashCode(ids);
    result = 31 * result + Arrays.hashCode(durationsUs);
    result = 31 * result + Arrays.hashCode(defaultPositionsUs);
    result = 31 * result + Arrays.hashCode(isLive);
    return result;
  }
}