public class

FakeMultiPeriodLiveTimeline

extends Timeline

 java.lang.Object

androidx.media3.common.Timeline

↳androidx.media3.test.utils.FakeMultiPeriodLiveTimeline

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

A fake Timeline that produces a live window with periods according to the available time range.

The parameters passed to the constructor define the availability start time, the window size and now. Use FakeMultiPeriodLiveTimeline.advanceNowUs(long) to advance the live window of the timeline accordingly.

The first available period with 0 (zero) starts at availabilityStartTimeUs. The live window starts at now - liveWindowDurationUs with the first period of the window having its ID relative to the first available period.

Periods are either of type content or ad as defined by the ad sequence pattern. A period is an ad if adSequencePattern[id % adSequencePattern.length] evaluates to true. Ad periods have a duration of FakeMultiPeriodLiveTimeline.AD_PERIOD_DURATION_MS and content periods have a duration of FakeMultiPeriodLiveTimeline.PERIOD_DURATION_MS.

Summary

Fields
public static final longAD_PERIOD_DURATION_MS

public static final longPERIOD_DURATION_MS

from TimelineEMPTY
Constructors
publicFakeMultiPeriodLiveTimeline(long availabilityStartTimeMs, long liveWindowDurationUs, long nowUs, boolean[] adSequencePattern[], long[] periodDurationMsPattern[], boolean isContentTimeline, boolean populateAds, boolean playedAds)

Creates an instance.

Methods
public voidadvanceNowUs(long durationUs)

Advances the live window by the given duration, in microseconds.

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 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 longgetPeriodStartTimeUs(int periodIndex)

Returns the period start time since Unix epoch, in microseconds.

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.

public longgetWindowStartTimeUs()

The window's start time in microseconds since the Unix epoch, or C.TIME_UNSET if unknown or not applicable.

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

Fields

public static final long AD_PERIOD_DURATION_MS

public static final long PERIOD_DURATION_MS

Constructors

public FakeMultiPeriodLiveTimeline(long availabilityStartTimeMs, long liveWindowDurationUs, long nowUs, boolean[] adSequencePattern[], long[] periodDurationMsPattern[], boolean isContentTimeline, boolean populateAds, boolean playedAds)

Creates an instance.

Parameters:

availabilityStartTimeMs: The start time of the available time range, UNIX epoch in milliseconds.
liveWindowDurationUs: The duration of the live window.
nowUs: The current time that determines the end of the live window.
adSequencePattern: The repeating pattern of periods starting at availabilityStartTimeMs. True is an ad period, and false a content period.
periodDurationMsPattern: The repeating pattern of periods durations starting at availabilityStartTimeMs, in milliseconds. Must have the same length as adSequencePattern.
isContentTimeline: Whether the timeline is a content timeline without AdPlaybackStates.
populateAds: Whether to populate ads in the same way if an ad event has been received.
playedAds: Whether ads should be marked as played if populated.

Methods

public void advanceNowUs(long durationUs)

Advances the live window by the given duration, in microseconds.

public long getWindowStartTimeUs()

The window's start time in microseconds since the Unix epoch, or C.TIME_UNSET if unknown or not applicable.

public long getPeriodStartTimeUs(int periodIndex)

Returns the period start time since Unix epoch, in microseconds.

Note: The returned value has millisecond precision only, so the trailing 3 digits are always zeros.

public abstract int getWindowCount()

Returns the number of windows in the timeline.

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.

Source

/*
 * Copyright (C) 2023 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.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Util.msToUs;
import static androidx.media3.common.util.Util.sum;
import static androidx.media3.common.util.Util.usToMs;

import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;

/**
 * A fake {@link Timeline} that produces a live window with periods according to the available time
 * range.
 *
 * <p>The parameters passed to the {@linkplain #FakeMultiPeriodLiveTimeline constructor} define the
 * availability start time, the window size and {@code now}. Use {@link #advanceNowUs(long)} to
 * advance the live window of the timeline accordingly.
 *
 * <p>The first available period with {@link Period#id ID} 0 (zero) starts at {@code
 * availabilityStartTimeUs}. The {@link Window live window} starts at {@code now -
 * liveWindowDurationUs} with the first period of the window having its ID relative to the first
 * available period.
 *
 * <p>Periods are either of type content or ad as defined by the ad sequence pattern. A period is an
 * ad if {@code adSequencePattern[id % adSequencePattern.length]} evaluates to true. Ad periods have
 * a duration of {@link #AD_PERIOD_DURATION_MS} and content periods have a duration of {@link
 * #PERIOD_DURATION_MS}.
 */
@UnstableApi
public class FakeMultiPeriodLiveTimeline extends Timeline {

  public static final long AD_PERIOD_DURATION_MS = 10_000L;
  public static final long PERIOD_DURATION_MS = 30_000L;

  private final boolean[] adSequencePattern;
  private final MediaItem mediaItem;
  private final long availabilityStartTimeUs;
  private final long liveWindowDurationUs;
  private final boolean isContentTimeline;
  private final boolean populateAds;
  private final boolean playedAds;
  private final long[] periodDurationUsPattern;

  private long nowUs;
  private ImmutableList<PeriodData> periods;

  /**
   * Creates an instance.
   *
   * @param availabilityStartTimeMs The start time of the available time range, UNIX epoch in
   *     milliseconds.
   * @param liveWindowDurationUs The duration of the live window.
   * @param nowUs The current time that determines the end of the live window.
   * @param adSequencePattern The repeating pattern of periods starting at {@code
   *     availabilityStartTimeMs}. True is an ad period, and false a content period.
   * @param periodDurationMsPattern The repeating pattern of periods durations starting at {@code
   *     availabilityStartTimeMs}, in milliseconds. Must have the same length as {@code
   *     adSequencePattern}.
   * @param isContentTimeline Whether the timeline is a content timeline without {@link
   *     AdPlaybackState}s.
   * @param populateAds Whether to populate ads in the same way if an ad event has been received.
   * @param playedAds Whether ads should be marked as played if populated.
   */
  public FakeMultiPeriodLiveTimeline(
      long availabilityStartTimeMs,
      long liveWindowDurationUs,
      long nowUs,
      boolean[] adSequencePattern,
      long[] periodDurationMsPattern,
      boolean isContentTimeline,
      boolean populateAds,
      boolean playedAds) {
    checkArgument(nowUs - liveWindowDurationUs >= msToUs(availabilityStartTimeMs));
    checkArgument(adSequencePattern.length == periodDurationMsPattern.length);
    this.availabilityStartTimeUs = msToUs(availabilityStartTimeMs);
    this.liveWindowDurationUs = liveWindowDurationUs;
    this.nowUs = nowUs;
    this.adSequencePattern = Arrays.copyOf(adSequencePattern, adSequencePattern.length);
    periodDurationUsPattern = new long[periodDurationMsPattern.length];
    for (int i = 0; i < periodDurationMsPattern.length; i++) {
      periodDurationUsPattern[i] = msToUs(periodDurationMsPattern[i]);
    }
    this.isContentTimeline = isContentTimeline;
    this.populateAds = populateAds;
    this.playedAds = playedAds;
    mediaItem = new MediaItem.Builder().build();
    periods =
        invalidate(
            msToUs(availabilityStartTimeMs),
            liveWindowDurationUs,
            nowUs,
            adSequencePattern,
            periodDurationUsPattern,
            isContentTimeline,
            populateAds,
            playedAds);
  }

  /** Advances the live window by the given duration, in microseconds. */
  public void advanceNowUs(long durationUs) {
    nowUs += durationUs;
    periods =
        invalidate(
            availabilityStartTimeUs,
            liveWindowDurationUs,
            nowUs,
            adSequencePattern,
            periodDurationUsPattern,
            isContentTimeline,
            populateAds,
            playedAds);
  }

  /**
   * The window's start time in microseconds since the Unix epoch, or {@link C#TIME_UNSET} if
   * unknown or not applicable.
   */
  public long getWindowStartTimeUs() {
    Window window = getWindow(/* windowIndex= */ 0, new Window());
    // Revert us/ms truncation introduced in `getWindow()`. This is identical to the truncation
    // applied in the Media3 `DashMediaSource.DashTimeline` and can be reverted in the same way.
    return window.windowStartTimeMs != C.TIME_UNSET
        ? msToUs(window.windowStartTimeMs) + (window.positionInFirstPeriodUs % 1000)
        : C.TIME_UNSET;
  }

  /**
   * Returns the period start time since Unix epoch, in microseconds.
   *
   * <p>Note: The returned value has millisecond precision only, so the trailing 3 digits are always
   * zeros.
   */
  public long getPeriodStartTimeUs(int periodIndex) {
    return msToUs(periods.get(periodIndex).periodStartTimeMs);
  }

  @Override
  public int getWindowCount() {
    return 1;
  }

  @Override
  public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
    checkArgument(windowIndex == 0);
    MediaItem.LiveConfiguration liveConfiguration =
        new MediaItem.LiveConfiguration.Builder().build();
    long positionInFirstPeriodUs = -periods.get(0).positionInWindowUs;
    window.set(
        /* uid= */ "live-window",
        mediaItem,
        /* manifest= */ null,
        /* presentationStartTimeMs= */ C.TIME_UNSET,
        /* windowStartTimeMs= */ periods.get(0).periodStartTimeMs + usToMs(positionInFirstPeriodUs),
        /* elapsedRealtimeEpochOffsetMs= */ 0,
        /* isSeekable= */ true,
        /* isDynamic= */ true,
        liveConfiguration,
        /* defaultPositionUs= */ liveWindowDurationUs - msToUs(liveConfiguration.targetOffsetMs),
        /* durationUs= */ liveWindowDurationUs,
        /* firstPeriodIndex= */ 0,
        /* lastPeriodIndex= */ getPeriodCount() - 1,
        positionInFirstPeriodUs);
    return window;
  }

  @Override
  public int getPeriodCount() {
    return periods.size();
  }

  @Override
  public Period getPeriod(int periodIndex, Period period, boolean setIds) {
    PeriodData periodData = periods.get(periodIndex);
    period.set(
        periodData.id,
        periodData.uid,
        /* windowIndex= */ 0,
        /* durationUs= */ periodIndex < getPeriodCount() - 1 ? periodData.durationUs : C.TIME_UNSET,
        periodData.positionInWindowUs,
        periodData.adPlaybackState,
        /* isPlaceholder= */ false);
    return period;
  }

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

  @Override
  public Object getUidOfPeriod(int periodIndex) {
    return periods.get(periodIndex).uid;
  }

  private static ImmutableList<PeriodData> invalidate(
      long availabilityStartTimeUs,
      long liveWindowDurationUs,
      long now,
      boolean[] adSequencePattern,
      long[] periodDurationUsPattern,
      boolean isContentTimeline,
      boolean populateAds,
      boolean playedAds) {
    long windowStartTimeUs = now - liveWindowDurationUs;
    int sequencePeriodCount = adSequencePattern.length;
    long sequenceDurationUs = sum(periodDurationUsPattern);
    long skippedSequenceCount = (windowStartTimeUs - availabilityStartTimeUs) / sequenceDurationUs;
    // Search the first period of the live window.
    int firstPeriodIndex = (int) (skippedSequenceCount * sequencePeriodCount);
    long firstPeriodDurationUs = periodDurationUsPattern[firstPeriodIndex % sequencePeriodCount];
    long firstPeriodEndTimeUs =
        availabilityStartTimeUs
            + (sequenceDurationUs * skippedSequenceCount)
            + firstPeriodDurationUs;
    while (firstPeriodEndTimeUs <= windowStartTimeUs) {
      firstPeriodDurationUs = periodDurationUsPattern[++firstPeriodIndex % sequencePeriodCount];
      firstPeriodEndTimeUs += firstPeriodDurationUs;
    }
    ImmutableList.Builder<PeriodData> liveWindow = new ImmutableList.Builder<>();
    long lastPeriodStartTimeUs = firstPeriodEndTimeUs - firstPeriodDurationUs;
    int lastPeriodIndex = firstPeriodIndex;
    // Add periods to the window from the first period until we find a period start after `now`.
    while (lastPeriodStartTimeUs < now) {
      long periodDurationUs = periodDurationUsPattern[lastPeriodIndex % sequencePeriodCount];
      boolean isAd = adSequencePattern[lastPeriodIndex % sequencePeriodCount];
      AdPlaybackState adPlaybackState = AdPlaybackState.NONE;
      if (!isContentTimeline) {
        adPlaybackState = new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended();
        if (isAd && populateAds) {
          adPlaybackState =
              adPlaybackState
                  .withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ 0)
                  .withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true)
                  .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
                  .withAdDurationsUs(
                      /* adGroupIndex= */ 0, /* adDurationsUs...= */ periodDurationUs)
                  .withContentResumeOffsetUs(
                      /* adGroupIndex= */ 0, /* contentResumeOffsetUs= */ periodDurationUs);
          if (playedAds) {
            adPlaybackState =
                adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
          }
        }
      }
      liveWindow.add(
          new PeriodData(
              /* id= */ lastPeriodIndex++,
              periodDurationUs,
              /* positionInWindowUs= */ lastPeriodStartTimeUs - windowStartTimeUs,
              /* periodStartTimeMs= */ usToMs(lastPeriodStartTimeUs),
              isAd,
              adPlaybackState));
      lastPeriodStartTimeUs += periodDurationUs;
    }
    return liveWindow.build();
  }

  private static class PeriodData {

    private final int id;
    private final Object uid;
    private final long durationUs;
    private final long positionInWindowUs;
    private final long periodStartTimeMs;
    private final AdPlaybackState adPlaybackState;

    /** Creates an instance. */
    public PeriodData(
        int id,
        long durationUs,
        long positionInWindowUs,
        long periodStartTimeMs,
        boolean isAd,
        AdPlaybackState adPlaybackState) {
      this.id = id;
      this.periodStartTimeMs = periodStartTimeMs;
      this.uid = "uid-" + id + "[" + (isAd ? "a" : "c") + "]";
      this.durationUs = durationUs;
      this.positionInWindowUs = positionInWindowUs;
      this.adPlaybackState = adPlaybackState;
    }
  }
}