public final class

FakeTimeline

extends Timeline

 java.lang.Object

androidx.media3.common.Timeline

↳androidx.media3.test.utils.FakeTimeline

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-test-utils', version: '1.5.0-alpha01'

  • groupId: androidx.media3
  • artifactId: media3-test-utils
  • version: 1.5.0-alpha01

Artifact androidx.media3:media3-test-utils:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)

Overview

Fake Timeline which can be setup to return custom FakeTimeline.TimelineWindowDefinitions.

Summary

Fields
public static final MediaItemFAKE_MEDIA_ITEM

The fake media item used by the fake timeline.

from TimelineEMPTY
Constructors
publicFakeTimeline()

Create a fake timeline with one seekable, non-dynamic window with one period and a duration of FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US.

publicFakeTimeline(FakeTimeline.TimelineWindowDefinition windowDefinitions[])

Creates a fake timeline with the given window definitions.

publicFakeTimeline(int windowCount, java.lang.Object manifests[])

Creates a fake timeline with the given number of seekable, non-dynamic windows with one period with a duration of FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US each.

publicFakeTimeline(java.lang.Object manifests[], FakeTimeline.TimelineWindowDefinition windowDefinitions[])

Creates a fake timeline with the given window definitions.

publicFakeTimeline(java.lang.Object manifests[], ShuffleOrder shuffleOrder, FakeTimeline.TimelineWindowDefinition windowDefinitions[])

Creates a fake timeline with the given window definitions and ShuffleOrder.

Methods
public static AdPlaybackStatecreateAdPlaybackState(int adsPerAdGroup, long[] adGroupTimesUs[])

Returns an ad playback state with the specified number of ads in each of the specified ad groups, each ten seconds long.

public static FakeTimelinecreateMultiPeriodAdTimeline(java.lang.Object windowId, int numberOfPlayedAds, boolean[] isAdPeriodFlags[])

Creates a multi-period timeline with ad and content periods specified by the flags passed as var-arg arguments.

public <any>getAdPlaybackStates(int windowIndex)

Returns a map of ad playback states keyed by the period UID.

public intgetFirstWindowIndex(boolean shuffleModeEnabled)

Returns the index of the first window in the playback order depending on whether shuffling is enabled.

public abstract intgetIndexOfPeriod(java.lang.Object uid)

Returns the index of the period identified by its unique Timeline.Period.uid, or C.INDEX_UNSET if the period is not in the timeline.

public intgetLastWindowIndex(boolean shuffleModeEnabled)

Returns the index of the last window in the playback order depending on whether shuffling is enabled.

public intgetNextWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled)

Returns the index of the window after the window at index windowIndex depending on the repeatMode and whether shuffling is enabled.

public abstract Timeline.PeriodgetPeriod(int periodIndex, Timeline.Period period, boolean setIds)

Populates a Timeline.Period with data for the period at the specified index.

public abstract intgetPeriodCount()

Returns the number of periods in the timeline.

public intgetPreviousWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled)

Returns the index of the window before the window at index windowIndex depending on the repeatMode and whether shuffling is enabled.

public abstract java.lang.ObjectgetUidOfPeriod(int periodIndex)

Returns the unique id of the period identified by its index in the timeline.

public abstract Timeline.WindowgetWindow(int windowIndex, Timeline.Window window, long defaultPositionProjectionUs)

Populates a Timeline.Window with data for the window at the specified index.

public abstract intgetWindowCount()

Returns the number of windows in the timeline.

from TimelinecopyWithSingleWindow, equals, fromBundle, getNextPeriodIndex, getPeriod, getPeriodByUid, getPeriodPosition, getPeriodPosition, getPeriodPositionUs, getPeriodPositionUs, getWindow, hashCode, isEmpty, isLastPeriod, toBundle
from java.lang.Objectclone, finalize, getClass, notify, notifyAll, toString, wait, wait, wait

Fields

public static final MediaItem FAKE_MEDIA_ITEM

The fake media item used by the fake timeline.

Constructors

public FakeTimeline()

Create a fake timeline with one seekable, non-dynamic window with one period and a duration of FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US.

public FakeTimeline(int windowCount, java.lang.Object manifests[])

Creates a fake timeline with the given number of seekable, non-dynamic windows with one period with a duration of FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US each.

Parameters:

windowCount: The number of windows.
manifests: The manifests of the windows.

public FakeTimeline(FakeTimeline.TimelineWindowDefinition windowDefinitions[])

Creates a fake timeline with the given window definitions.

Parameters:

windowDefinitions: A list of FakeTimeline.TimelineWindowDefinitions.

public FakeTimeline(java.lang.Object manifests[], FakeTimeline.TimelineWindowDefinition windowDefinitions[])

Creates a fake timeline with the given window definitions.

Parameters:

manifests: The manifests of the windows.
windowDefinitions: A list of FakeTimeline.TimelineWindowDefinitions.

public FakeTimeline(java.lang.Object manifests[], ShuffleOrder shuffleOrder, FakeTimeline.TimelineWindowDefinition windowDefinitions[])

Creates a fake timeline with the given window definitions and ShuffleOrder.

Parameters:

manifests: The manifests of the windows.
shuffleOrder: A shuffle ordering for the windows.
windowDefinitions: A list of FakeTimeline.TimelineWindowDefinitions.

Methods

public static AdPlaybackState createAdPlaybackState(int adsPerAdGroup, long[] adGroupTimesUs[])

Returns an ad playback state with the specified number of ads in each of the specified ad groups, each ten seconds long.

Parameters:

adsPerAdGroup: The number of ads per ad group.
adGroupTimesUs: The times of ad groups, in microseconds.

Returns:

The ad playback state.

public static FakeTimeline createMultiPeriodAdTimeline(java.lang.Object windowId, int numberOfPlayedAds, boolean[] isAdPeriodFlags[])

Creates a multi-period timeline with ad and content periods specified by the flags passed as var-arg arguments.

Period uid end up being a new Pair<>(windowId, periodIndex).

Parameters:

windowId: The window ID.
numberOfPlayedAds: The number of ads that should be marked as played.
isAdPeriodFlags: A value of true indicates an ad period. A value of false indicated a content period.

Returns:

A timeline with a single window with as many periods as var-arg arguments.

public abstract int getWindowCount()

Returns the number of windows in the timeline.

public int getNextWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled)

Returns the index of the window after the window at index windowIndex depending on the repeatMode and whether shuffling is enabled.

Parameters:

windowIndex: Index of a window in the timeline.
repeatMode: A repeat mode.
shuffleModeEnabled: Whether shuffling is enabled.

Returns:

The index of the next window, or C.INDEX_UNSET if this is the last window.

public int getPreviousWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled)

Returns the index of the window before the window at index windowIndex depending on the repeatMode and whether shuffling is enabled.

Parameters:

windowIndex: Index of a window in the timeline.
repeatMode: A repeat mode.
shuffleModeEnabled: Whether shuffling is enabled.

Returns:

The index of the previous window, or C.INDEX_UNSET if this is the first window.

public int getLastWindowIndex(boolean shuffleModeEnabled)

Returns the index of the last window in the playback order depending on whether shuffling is enabled.

Parameters:

shuffleModeEnabled: Whether shuffling is enabled.

Returns:

The index of the last window in the playback order, or C.INDEX_UNSET if the timeline is empty.

public int getFirstWindowIndex(boolean shuffleModeEnabled)

Returns the index of the first window in the playback order depending on whether shuffling is enabled.

Parameters:

shuffleModeEnabled: Whether shuffling is enabled.

Returns:

The index of the first window in the playback order, or C.INDEX_UNSET if the timeline is empty.

public abstract Timeline.Window getWindow(int windowIndex, Timeline.Window window, long defaultPositionProjectionUs)

Populates a Timeline.Window with data for the window at the specified index.

Parameters:

windowIndex: The index of the window.
window: The Timeline.Window to populate. Must not be null.
defaultPositionProjectionUs: A duration into the future that the populated window's default start position should be projected.

Returns:

The populated Timeline.Window, for convenience.

public abstract int getPeriodCount()

Returns the number of periods in the timeline.

public abstract Timeline.Period getPeriod(int periodIndex, Timeline.Period period, boolean setIds)

Populates a Timeline.Period with data for the period at the specified index.

Parameters:

periodIndex: The index of the period.
period: The Timeline.Period to populate. Must not be null.
setIds: Whether Timeline.Period.id and Timeline.Period.uid should be populated. If false, the fields will be set to null. The caller should pass false for efficiency reasons unless the fields are required.

Returns:

The populated Timeline.Period, for convenience.

public abstract int getIndexOfPeriod(java.lang.Object uid)

Returns the index of the period identified by its unique Timeline.Period.uid, or C.INDEX_UNSET if the period is not in the timeline.

Parameters:

uid: A unique identifier for a period.

Returns:

The index of the period, or C.INDEX_UNSET if the period was not found.

public abstract java.lang.Object getUidOfPeriod(int periodIndex)

Returns the unique id of the period identified by its index in the timeline.

Parameters:

periodIndex: The index of the period.

Returns:

The unique id of the period.

public <any> getAdPlaybackStates(int windowIndex)

Returns a map of ad playback states keyed by the period UID.

Parameters:

windowIndex: The window index of the window to get the map of ad playback states from.

Returns:

The map of ad playback states.

Source

/*
 * 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.test.utils;

import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
import static java.lang.Math.min;

import android.net.Uri;
import android.util.Pair;
import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.source.ShuffleOrder;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** Fake {@link Timeline} which can be setup to return custom {@link TimelineWindowDefinition}s. */
@UnstableApi
public final class FakeTimeline extends Timeline {

  /** Definition used to define a {@link FakeTimeline}. */
  public static final class TimelineWindowDefinition {

    /** Default window duration in microseconds. */
    public static final long DEFAULT_WINDOW_DURATION_US = 10 * C.MICROS_PER_SECOND;

    /** Default offset of a window in its first period in microseconds. */
    public static final long DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US = 123 * C.MICROS_PER_SECOND;

    public final int periodCount;
    public final Object id;
    public final MediaItem mediaItem;
    public final boolean isSeekable;
    public final boolean isDynamic;
    public final boolean isLive;
    public final boolean isPlaceholder;
    public final long durationUs;
    public final long defaultPositionUs;
    public final long windowOffsetInFirstPeriodUs;
    public final List<AdPlaybackState> adPlaybackStates;

    /**
     * Creates a window definition that corresponds to a placeholder timeline using the given tag.
     *
     * @param tag The tag to use in the timeline.
     */
    public static TimelineWindowDefinition createPlaceholder(Object tag) {
      return new TimelineWindowDefinition(
          /* periodCount= */ 1,
          /* id= */ tag,
          /* isSeekable= */ false,
          /* isDynamic= */ true,
          /* isLive= */ false,
          /* isPlaceholder= */ true,
          /* durationUs= */ C.TIME_UNSET,
          /* defaultPositionUs= */ 0,
          /* windowOffsetInFirstPeriodUs= */ 0,
          AdPlaybackState.NONE);
    }

    /**
     * Creates a seekable, non-dynamic window definition with a duration of {@link
     * #DEFAULT_WINDOW_DURATION_US}.
     *
     * @param periodCount The number of periods in the window. Each period get an equal slice of the
     *     total window duration.
     * @param id The UID of the window.
     */
    public TimelineWindowDefinition(int periodCount, Object id) {
      this(periodCount, id, true, false, DEFAULT_WINDOW_DURATION_US);
    }

    /**
     * Creates a window definition with one period.
     *
     * @param isSeekable Whether the window is seekable.
     * @param isDynamic Whether the window is dynamic.
     * @param durationUs The duration of the window in microseconds.
     */
    public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) {
      this(1, 0, isSeekable, isDynamic, durationUs);
    }

    /**
     * Creates a window definition.
     *
     * @param periodCount The number of periods in the window. Each period get an equal slice of the
     *     total window duration.
     * @param id The UID of the window.
     * @param isSeekable Whether the window is seekable.
     * @param isDynamic Whether the window is dynamic.
     * @param durationUs The duration of the window in microseconds.
     */
    public TimelineWindowDefinition(
        int periodCount, Object id, boolean isSeekable, boolean isDynamic, long durationUs) {
      this(periodCount, id, isSeekable, isDynamic, durationUs, AdPlaybackState.NONE);
    }

    /**
     * Creates a window definition with ad groups.
     *
     * @param periodCount The number of periods in the window. Each period get an equal slice of the
     *     total window duration.
     * @param id The UID of the window.
     * @param isSeekable Whether the window is seekable.
     * @param isDynamic Whether the window is dynamic.
     * @param durationUs The duration of the window in microseconds.
     * @param adPlaybackState The ad playback state.
     */
    public TimelineWindowDefinition(
        int periodCount,
        Object id,
        boolean isSeekable,
        boolean isDynamic,
        long durationUs,
        AdPlaybackState adPlaybackState) {
      this(
          periodCount,
          id,
          isSeekable,
          isDynamic,
          /* isLive= */ isDynamic,
          /* isPlaceholder= */ false,
          durationUs,
          /* defaultPositionUs= */ 0,
          DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
          adPlaybackState);
    }

    /**
     * Creates a window definition with ad groups.
     *
     * @param periodCount The number of periods in the window. Each period get an equal slice of the
     *     total window duration.
     * @param id The UID of the window.
     * @param isSeekable Whether the window is seekable.
     * @param isDynamic Whether the window is dynamic.
     * @param isLive Whether the window is live.
     * @param isPlaceholder Whether the window is a placeholder.
     * @param durationUs The duration of the window in microseconds.
     * @param defaultPositionUs The default position of the window in microseconds.
     * @param windowOffsetInFirstPeriodUs The offset of the window in the first period, in
     *     microseconds.
     * @param adPlaybackState The ad playback state.
     */
    public TimelineWindowDefinition(
        int periodCount,
        Object id,
        boolean isSeekable,
        boolean isDynamic,
        boolean isLive,
        boolean isPlaceholder,
        long durationUs,
        long defaultPositionUs,
        long windowOffsetInFirstPeriodUs,
        AdPlaybackState adPlaybackState) {
      this(
          periodCount,
          id,
          isSeekable,
          isDynamic,
          isLive,
          isPlaceholder,
          durationUs,
          defaultPositionUs,
          windowOffsetInFirstPeriodUs,
          ImmutableList.of(adPlaybackState),
          FAKE_MEDIA_ITEM.buildUpon().setTag(id).build());
    }

    /**
     * @deprecated Use {@link #TimelineWindowDefinition(int, Object, boolean, boolean, boolean,
     *     boolean, long, long, long, List, MediaItem)} instead.
     */
    @Deprecated
    public TimelineWindowDefinition(
        int periodCount,
        Object id,
        boolean isSeekable,
        boolean isDynamic,
        boolean isLive,
        boolean isPlaceholder,
        long durationUs,
        long defaultPositionUs,
        long windowOffsetInFirstPeriodUs,
        AdPlaybackState adPlaybackState,
        MediaItem mediaItem) {
      this(
          periodCount,
          id,
          isSeekable,
          isDynamic,
          isLive,
          isPlaceholder,
          durationUs,
          defaultPositionUs,
          windowOffsetInFirstPeriodUs,
          ImmutableList.of(adPlaybackState),
          mediaItem);
    }

    /**
     * Creates a window definition with ad groups and a custom media item.
     *
     * @param periodCount The number of periods in the window. Each period get an equal slice of the
     *     total window duration.
     * @param id The UID of the window.
     * @param isSeekable Whether the window is seekable.
     * @param isDynamic Whether the window is dynamic.
     * @param isLive Whether the window is live.
     * @param isPlaceholder Whether the window is a placeholder.
     * @param durationUs The duration of the window in microseconds.
     * @param defaultPositionUs The default position of the window in microseconds.
     * @param windowOffsetInFirstPeriodUs The offset of the window in the first period, in
     *     microseconds.
     * @param adPlaybackStates The ad playback states for the periods.
     * @param mediaItem The media item to include in the timeline.
     */
    public TimelineWindowDefinition(
        int periodCount,
        Object id,
        boolean isSeekable,
        boolean isDynamic,
        boolean isLive,
        boolean isPlaceholder,
        long durationUs,
        long defaultPositionUs,
        long windowOffsetInFirstPeriodUs,
        List<AdPlaybackState> adPlaybackStates,
        MediaItem mediaItem) {
      Assertions.checkArgument(durationUs != C.TIME_UNSET || periodCount == 1);
      this.periodCount = periodCount;
      this.id = id;
      this.mediaItem = mediaItem;
      this.isSeekable = isSeekable;
      this.isDynamic = isDynamic;
      this.isLive = isLive;
      this.isPlaceholder = isPlaceholder;
      this.durationUs = durationUs;
      this.defaultPositionUs = defaultPositionUs;
      this.windowOffsetInFirstPeriodUs = windowOffsetInFirstPeriodUs;
      this.adPlaybackStates = adPlaybackStates;
    }
  }

  /** The fake media item used by the fake timeline. */
  public static final MediaItem FAKE_MEDIA_ITEM =
      new MediaItem.Builder().setMediaId("FakeTimeline").setUri(Uri.EMPTY).build();

  private static final long AD_DURATION_US = 5 * C.MICROS_PER_SECOND;

  private final TimelineWindowDefinition[] windowDefinitions;
  private final Object[] manifests;
  private final int[] periodOffsets;
  private final ShuffleOrder shuffleOrder;

  /**
   * Returns an ad playback state with the specified number of ads in each of the specified ad
   * groups, each ten seconds long.
   *
   * @param adsPerAdGroup The number of ads per ad group.
   * @param adGroupTimesUs The times of ad groups, in microseconds.
   * @return The ad playback state.
   */
  public static AdPlaybackState createAdPlaybackState(int adsPerAdGroup, long... adGroupTimesUs) {
    int adGroupCount = adGroupTimesUs.length;
    AdPlaybackState adPlaybackState =
        new AdPlaybackState(/* adsId= */ new Object(), adGroupTimesUs);
    long[][] adDurationsUs = new long[adGroupCount][];
    for (int i = 0; i < adGroupCount; i++) {
      adPlaybackState = adPlaybackState.withAdCount(/* adGroupIndex= */ i, adsPerAdGroup);
      for (int j = 0; j < adsPerAdGroup; j++) {
        adPlaybackState =
            adPlaybackState.withAvailableAdMediaItem(
                /* adGroupIndex= */ i,
                /* adIndexInAdGroup= */ j,
                MediaItem.fromUri("https://example.com/ad/" + i + "/" + j));
      }
      adDurationsUs[i] = new long[adsPerAdGroup];
      Arrays.fill(adDurationsUs[i], AD_DURATION_US);
    }
    adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);

    return adPlaybackState;
  }

  /**
   * Creates a multi-period timeline with ad and content periods specified by the flags passed as
   * var-arg arguments.
   *
   * <p>Period uid end up being a {@code new Pair<>(windowId, periodIndex)}.
   *
   * @param windowId The window ID.
   * @param numberOfPlayedAds The number of ads that should be marked as played.
   * @param isAdPeriodFlags A value of true indicates an ad period. A value of false indicated a
   *     content period.
   * @return A timeline with a single window with as many periods as var-arg arguments.
   */
  public static FakeTimeline createMultiPeriodAdTimeline(
      Object windowId, int numberOfPlayedAds, boolean... isAdPeriodFlags) {
    long periodDurationUs = DEFAULT_WINDOW_DURATION_US / isAdPeriodFlags.length;
    AdPlaybackState contentPeriodState = new AdPlaybackState(/* adsId= */ "adsId");
    AdPlaybackState firstAdPeriodState =
        contentPeriodState
            .withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ 0)
            .withAdCount(/* adGroupIndex= */ 0, 1)
            .withAdDurationsUs(
                /* adGroupIndex= */ 0, DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs)
            .withIsServerSideInserted(/* adGroupIndex= */ 0, true);
    AdPlaybackState commonAdPeriodState = firstAdPeriodState.withAdDurationsUs(0, periodDurationUs);

    List<AdPlaybackState> adPlaybackStates = new ArrayList<>();
    int playedAdsCounter = 0;
    for (boolean isAd : isAdPeriodFlags) {
      AdPlaybackState periodAdPlaybackState =
          isAd
              ? (adPlaybackStates.isEmpty() ? firstAdPeriodState : commonAdPeriodState)
              : contentPeriodState;
      if (isAd && playedAdsCounter < numberOfPlayedAds) {
        periodAdPlaybackState =
            periodAdPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
        playedAdsCounter++;
      }
      adPlaybackStates.add(periodAdPlaybackState);
    }
    return new FakeTimeline(
        new FakeTimeline.TimelineWindowDefinition(
            isAdPeriodFlags.length,
            windowId,
            /* isSeekable= */ true,
            /* isDynamic= */ false,
            /* isLive= */ false,
            /* isPlaceholder= */ false,
            /* durationUs= */ DEFAULT_WINDOW_DURATION_US,
            /* defaultPositionUs= */ 0,
            /* windowOffsetInFirstPeriodUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
            /* adPlaybackStates= */ adPlaybackStates,
            MediaItem.EMPTY));
  }

  /**
   * Create a fake timeline with one seekable, non-dynamic window with one period and a duration of
   * {@link TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}.
   */
  public FakeTimeline() {
    this(/* windowCount= */ 1);
  }

  /**
   * Creates a fake timeline with the given number of seekable, non-dynamic windows with one period
   * with a duration of {@link TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US} each.
   *
   * @param windowCount The number of windows.
   * @param manifests The manifests of the windows.
   */
  public FakeTimeline(int windowCount, Object... manifests) {
    this(manifests, createDefaultWindowDefinitions(windowCount));
  }

  /**
   * Creates a fake timeline with the given window definitions.
   *
   * @param windowDefinitions A list of {@link TimelineWindowDefinition}s.
   */
  public FakeTimeline(TimelineWindowDefinition... windowDefinitions) {
    this(new Object[0], windowDefinitions);
  }

  /**
   * Creates a fake timeline with the given window definitions.
   *
   * @param manifests The manifests of the windows.
   * @param windowDefinitions A list of {@link TimelineWindowDefinition}s.
   */
  public FakeTimeline(Object[] manifests, TimelineWindowDefinition... windowDefinitions) {
    this(manifests, new FakeShuffleOrder(windowDefinitions.length), windowDefinitions);
  }

  /**
   * Creates a fake timeline with the given window definitions and {@link
   * androidx.media3.exoplayer.source.ShuffleOrder}.
   *
   * @param manifests The manifests of the windows.
   * @param shuffleOrder A shuffle ordering for the windows.
   * @param windowDefinitions A list of {@link TimelineWindowDefinition}s.
   */
  public FakeTimeline(
      Object[] manifests,
      ShuffleOrder shuffleOrder,
      TimelineWindowDefinition... windowDefinitions) {
    this.manifests = new Object[windowDefinitions.length];
    System.arraycopy(manifests, 0, this.manifests, 0, min(this.manifests.length, manifests.length));
    this.windowDefinitions = windowDefinitions;
    periodOffsets = new int[windowDefinitions.length + 1];
    periodOffsets[0] = 0;
    for (int i = 0; i < windowDefinitions.length; i++) {
      periodOffsets[i + 1] = periodOffsets[i] + windowDefinitions[i].periodCount;
    }
    this.shuffleOrder = shuffleOrder;
  }

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

  @Override
  public int getNextWindowIndex(
      int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
    if (repeatMode == Player.REPEAT_MODE_ONE) {
      return windowIndex;
    }
    if (windowIndex == getLastWindowIndex(shuffleModeEnabled)) {
      return repeatMode == Player.REPEAT_MODE_ALL
          ? getFirstWindowIndex(shuffleModeEnabled)
          : C.INDEX_UNSET;
    }
    return shuffleModeEnabled ? shuffleOrder.getNextIndex(windowIndex) : windowIndex + 1;
  }

  @Override
  public int getPreviousWindowIndex(
      int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
    if (repeatMode == Player.REPEAT_MODE_ONE) {
      return windowIndex;
    }
    if (windowIndex == getFirstWindowIndex(shuffleModeEnabled)) {
      return repeatMode == Player.REPEAT_MODE_ALL
          ? getLastWindowIndex(shuffleModeEnabled)
          : C.INDEX_UNSET;
    }
    return shuffleModeEnabled ? shuffleOrder.getPreviousIndex(windowIndex) : windowIndex - 1;
  }

  @Override
  public int getLastWindowIndex(boolean shuffleModeEnabled) {
    return shuffleModeEnabled
        ? shuffleOrder.getLastIndex()
        : super.getLastWindowIndex(/* shuffleModeEnabled= */ false);
  }

  @Override
  public int getFirstWindowIndex(boolean shuffleModeEnabled) {
    return shuffleModeEnabled
        ? shuffleOrder.getFirstIndex()
        : super.getFirstWindowIndex(/* shuffleModeEnabled= */ false);
  }

  @Override
  public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
    TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];
    long windowDurationUs = 0;
    Period period = new Period();
    for (int i = periodOffsets[windowIndex]; i < periodOffsets[windowIndex + 1]; i++) {
      long periodDurationUs = getPeriod(/* periodIndex= */ i, period).durationUs;
      if (i == periodOffsets[windowIndex] && periodDurationUs != 0) {
        windowDurationUs -= windowDefinition.windowOffsetInFirstPeriodUs;
      }
      if (periodDurationUs == C.TIME_UNSET) {
        windowDurationUs = C.TIME_UNSET;
        break;
      }
      windowDurationUs += periodDurationUs;
    }
    window.set(
        /* uid= */ windowDefinition.id,
        windowDefinition.mediaItem,
        manifests[windowIndex],
        /* presentationStartTimeMs= */ C.TIME_UNSET,
        /* windowStartTimeMs= */ windowDefinition.isLive
            ? Util.usToMs(windowDefinition.windowOffsetInFirstPeriodUs)
            : C.TIME_UNSET,
        /* elapsedRealtimeEpochOffsetMs= */ windowDefinition.isLive ? 0 : C.TIME_UNSET,
        windowDefinition.isSeekable,
        windowDefinition.isDynamic,
        windowDefinition.isLive ? windowDefinition.mediaItem.liveConfiguration : null,
        windowDefinition.defaultPositionUs,
        windowDurationUs,
        periodOffsets[windowIndex],
        periodOffsets[windowIndex + 1] - 1,
        windowDefinition.windowOffsetInFirstPeriodUs);
    window.isPlaceholder = windowDefinition.isPlaceholder;
    return window;
  }

  @Override
  public int getPeriodCount() {
    return periodOffsets[periodOffsets.length - 1];
  }

  @Override
  public Period getPeriod(int periodIndex, Period period, boolean setIds) {
    int windowIndex = Util.binarySearchFloor(periodOffsets, periodIndex, true, false);
    int windowPeriodIndex = periodIndex - periodOffsets[windowIndex];
    TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];
    Object id = setIds ? windowPeriodIndex : null;
    Object uid = setIds ? Pair.create(windowDefinition.id, windowPeriodIndex) : null;
    AdPlaybackState adPlaybackState =
        windowDefinition.adPlaybackStates.get(
            periodIndex % windowDefinition.adPlaybackStates.size());
    // Arbitrarily set period duration by distributing window duration equally among all periods.
    long periodDurationUs =
        periodIndex == windowDefinition.periodCount - 1
                && windowDefinition.durationUs == C.TIME_UNSET
            ? C.TIME_UNSET
            : (windowDefinition.durationUs / windowDefinition.periodCount);
    long positionInWindowUs;
    if (windowPeriodIndex == 0) {
      if (windowDefinition.durationUs != C.TIME_UNSET) {
        periodDurationUs += windowDefinition.windowOffsetInFirstPeriodUs;
      }
      positionInWindowUs = -windowDefinition.windowOffsetInFirstPeriodUs;
    } else {
      positionInWindowUs = periodDurationUs * windowPeriodIndex;
    }
    period.set(
        id,
        uid,
        windowIndex,
        periodDurationUs,
        positionInWindowUs,
        adPlaybackState,
        windowDefinition.isPlaceholder);
    return period;
  }

  @Override
  public int getIndexOfPeriod(Object uid) {
    for (int i = 0; i < getPeriodCount(); i++) {
      if (getUidOfPeriod(i).equals(uid)) {
        return i;
      }
    }
    return C.INDEX_UNSET;
  }

  @Override
  public Object getUidOfPeriod(int periodIndex) {
    Assertions.checkIndex(periodIndex, 0, getPeriodCount());
    int windowIndex =
        Util.binarySearchFloor(
            periodOffsets, periodIndex, /* inclusive= */ true, /* stayInBounds= */ false);
    int windowPeriodIndex = periodIndex - periodOffsets[windowIndex];
    TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];
    return Pair.create(windowDefinition.id, windowPeriodIndex);
  }

  /**
   * Returns a map of ad playback states keyed by the period UID.
   *
   * @param windowIndex The window index of the window to get the map of ad playback states from.
   * @return The map of {@link AdPlaybackState ad playback states}.
   */
  public ImmutableMap<Object, AdPlaybackState> getAdPlaybackStates(int windowIndex) {
    Map<Object, AdPlaybackState> adPlaybackStateMap = new HashMap<>();
    TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];
    for (int i = 0; i < windowDefinition.adPlaybackStates.size(); i++) {
      adPlaybackStateMap.put(
          new Pair<>(windowDefinition.id, i), windowDefinition.adPlaybackStates.get(i));
    }
    return ImmutableMap.copyOf(adPlaybackStateMap);
  }

  private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) {
    TimelineWindowDefinition[] windowDefinitions = new TimelineWindowDefinition[windowCount];
    for (int i = 0; i < windowCount; i++) {
      windowDefinitions[i] = new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ i);
    }
    return windowDefinitions;
  }
}