From 1760d63fc462c895a446b0b8c059ab1379f6a277 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 27 Feb 2023 18:28:04 +0000 Subject: [PATCH] Add FakeMultiPeriodLiveTimeline and test case This timeline will be used in unit test cases of follow-up CLs. It basically can be used to emulate the timeline created by a multi-period live media source when the real time advances. PiperOrigin-RevId: 512665552 --- .../utils/FakeMultiPeriodLiveTimeline.java | 213 +++++++++++ .../FakeMultiPeriodLiveTimelineTest.java | 356 ++++++++++++++++++ 2 files changed, 569 insertions(+) create mode 100644 libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimeline.java create mode 100644 libraries/test_utils/src/test/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimelineTest.java diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimeline.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimeline.java new file mode 100644 index 0000000000..cfc056c90d --- /dev/null +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimeline.java @@ -0,0 +1,213 @@ +/* + * 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.usToMs; + +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. + * + *

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. + * + *

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. + * + *

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_US} and content periods have a duration of {@link + * #PERIOD_DURATION_US}. + */ +@UnstableApi +public class FakeMultiPeriodLiveTimeline extends Timeline { + + public static final long AD_PERIOD_DURATION_US = 10_000_000L; + public static final long PERIOD_DURATION_US = 30_000_000L; + + private final boolean[] adSequencePattern; + private final MediaItem mediaItem; + private final long availabilityStartTimeUs; + private final long liveWindowDurationUs; + + private long nowUs; + private ImmutableList periods; + + /** + * Creates an instance. + * + * @param availabilityStartTimeUs The start time of the available time range, in UNIX epoch. + * @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 + * availabilityStartTimeUs}. True is an ad period, and false a content period. + */ + public FakeMultiPeriodLiveTimeline( + long availabilityStartTimeUs, + long liveWindowDurationUs, + long nowUs, + boolean[] adSequencePattern) { + checkArgument(nowUs - liveWindowDurationUs >= availabilityStartTimeUs); + this.availabilityStartTimeUs = availabilityStartTimeUs; + this.liveWindowDurationUs = liveWindowDurationUs; + this.nowUs = nowUs; + this.adSequencePattern = Arrays.copyOf(adSequencePattern, adSequencePattern.length); + mediaItem = new MediaItem.Builder().build(); + periods = invalidate(availabilityStartTimeUs, liveWindowDurationUs, nowUs, adSequencePattern); + } + + /** Calculates the total duration of the given ad period sequence. */ + public static long calculateAdSequencePatternDurationUs(boolean[] adSequencePattern) { + long durationUs = 0; + for (boolean isAd : adSequencePattern) { + durationUs += (isAd ? AD_PERIOD_DURATION_US : PERIOD_DURATION_US); + } + return durationUs; + } + + /** Advances the live window by the given duration, in microseconds. */ + public void advanceNowUs(long durationUs) { + nowUs += durationUs; + periods = invalidate(availabilityStartTimeUs, liveWindowDurationUs, nowUs, adSequencePattern); + } + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + MediaItem.LiveConfiguration liveConfiguration = + new MediaItem.LiveConfiguration.Builder().build(); + window.set( + /* uid= */ "live-window", + mediaItem, + /* manifest= */ null, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ usToMs(nowUs - liveWindowDurationUs), + /* elapsedRealtimeEpochOffsetMs= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ true, + liveConfiguration, + /* defaultPositionUs= */ liveWindowDurationUs - msToUs(liveConfiguration.targetOffsetMs), + /* durationUs= */ liveWindowDurationUs, + /* firstPeriodIndex= */ 0, + /* lastPeriodIndex= */ getPeriodCount() - 1, + /* positionInFirstPeriodUs= */ -periods.get(0).positionInWindowUs); + 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); + 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 invalidate( + long availabilityStartTimeUs, + long liveWindowDurationUs, + long now, + boolean[] adSequencePattern) { + long windowStartTimeUs = now - liveWindowDurationUs; + int sequencePeriodCount = adSequencePattern.length; + long sequenceDurationUs = calculateAdSequencePatternDurationUs(adSequencePattern); + long skippedSequenceCount = (windowStartTimeUs - availabilityStartTimeUs) / sequenceDurationUs; + // Search the first period of the live window. + int firstPeriodIndex = (int) (skippedSequenceCount * sequencePeriodCount); + boolean isAd = adSequencePattern[firstPeriodIndex % sequencePeriodCount]; + long firstPeriodDurationUs = isAd ? AD_PERIOD_DURATION_US : PERIOD_DURATION_US; + long firstPeriodEndTimeUs = + availabilityStartTimeUs + + (sequenceDurationUs * skippedSequenceCount) + + firstPeriodDurationUs; + while (firstPeriodEndTimeUs <= windowStartTimeUs) { + isAd = adSequencePattern[++firstPeriodIndex % sequencePeriodCount]; + firstPeriodDurationUs = isAd ? AD_PERIOD_DURATION_US : PERIOD_DURATION_US; + firstPeriodEndTimeUs += firstPeriodDurationUs; + } + ImmutableList.Builder 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) { + isAd = adSequencePattern[lastPeriodIndex % sequencePeriodCount]; + long periodDurationUs = isAd ? AD_PERIOD_DURATION_US : PERIOD_DURATION_US; + liveWindow.add( + new PeriodData( + /* id= */ lastPeriodIndex++, + isAd, + periodDurationUs, + /* positionInWindowUs= */ lastPeriodStartTimeUs - windowStartTimeUs)); + 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; + + /** Creates an instance. */ + public PeriodData(int id, boolean isAd, long durationUs, long positionInWindowUs) { + this.id = id; + this.uid = "uid-" + id + "[" + (isAd ? "a" : "c") + "]"; + this.durationUs = durationUs; + this.positionInWindowUs = positionInWindowUs; + } + } +} diff --git a/libraries/test_utils/src/test/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimelineTest.java b/libraries/test_utils/src/test/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimelineTest.java new file mode 100644 index 0000000000..2a1ac4a3b7 --- /dev/null +++ b/libraries/test_utils/src/test/java/androidx/media3/test/utils/FakeMultiPeriodLiveTimelineTest.java @@ -0,0 +1,356 @@ +/* + * 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.test.utils.FakeMultiPeriodLiveTimeline.AD_PERIOD_DURATION_US; +import static androidx.media3.test.utils.FakeMultiPeriodLiveTimeline.PERIOD_DURATION_US; +import static androidx.media3.test.utils.FakeMultiPeriodLiveTimeline.calculateAdSequencePatternDurationUs; +import static com.google.common.truth.Truth.assertThat; + +import androidx.media3.common.C; +import androidx.media3.common.Timeline; +import androidx.media3.common.util.Util; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link FakeMultiPeriodLiveTimeline}. */ +@RunWith(AndroidJUnit4.class) +public class FakeMultiPeriodLiveTimelineTest { + + private static final long A_DAY_US = 24L * 60L * 60L * 1_000_000L; + + @Test + public void newInstance_availabilitySinceStartOfUnixEpoch_correctLiveWindow() { + boolean[] adSequencePattern = {false, true, true}; + FakeMultiPeriodLiveTimeline timeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0L, + /* liveWindowDurationUs= */ 60_000_000L, + /* nowUs= */ 60_000_000L, + adSequencePattern); + Timeline.Period period = new Timeline.Period(); + Timeline.Window window = new Timeline.Window(); + + assertThat(timeline.getPeriodCount()).isEqualTo(4); + assertThat(timeline.getWindow(0, window).windowStartTimeMs).isEqualTo(0L); + assertThat(timeline.getPeriod(0, period).id).isEqualTo(0); + assertThat(timeline.getPeriod(0, period).uid).isEqualTo("uid-0[c]"); + assertThat(timeline.getPeriod(0, period).positionInWindowUs).isEqualTo(0L); + assertThat(timeline.getPeriod(0, period).durationUs).isEqualTo(30_000_000L); + assertThat(timeline.getPeriod(1, period).positionInWindowUs).isEqualTo(30_000_000L); + assertThat(timeline.getPeriod(1, period).durationUs).isEqualTo(10_000_000L); + assertThat(timeline.getPeriod(2, period).positionInWindowUs).isEqualTo(40_000_000L); + assertThat(timeline.getPeriod(2, period).durationUs).isEqualTo(10_000_000L); + assertThat(timeline.getPeriod(3, period).positionInWindowUs).isEqualTo(50_000_000L); + assertThat(timeline.getPeriod(3, period).durationUs).isEqualTo(C.TIME_UNSET); + assertExpectedWindow( + timeline, + calculateExpectedWindow( + /* availabilityStartTimeUs= */ 0L, + /* liveWindowDurationUs= */ 60_000_000L, + /* nowUs= */ 60_000_000L, + adSequencePattern), + adSequencePattern); + } + + @Test + public void advanceTimeUs_availabilitySinceStartOfUnixEpoch_correctPeriodsInLiveWindow() { + boolean[] adSequencePattern = {false, true, true}; + FakeMultiPeriodLiveTimeline timeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0L, + /* liveWindowDurationUs= */ 60_000_000L, + /* nowUs= */ 60_000_123L, + adSequencePattern); + Timeline.Period period = new Timeline.Period(); + Timeline.Window window = new Timeline.Window(); + + assertThat(timeline.getPeriodCount()).isEqualTo(4); + assertThat(timeline.getWindow(0, window).windowStartTimeMs).isEqualTo(0L); + assertThat(timeline.getPeriod(0, period).id).isEqualTo(0); + assertThat(timeline.getPeriod(0, period).uid).isEqualTo("uid-0[c]"); + assertThat(timeline.getPeriod(0, period).positionInWindowUs).isEqualTo(-123L); + assertThat(timeline.getPeriod(1, period).positionInWindowUs).isEqualTo(29_999_877L); + assertThat(timeline.getPeriod(2, period).positionInWindowUs).isEqualTo(39_999_877L); + assertThat(timeline.getPeriod(3, period).positionInWindowUs).isEqualTo(49_999_877L); + assertThat(timeline.getPeriod(3, period).durationUs).isEqualTo(C.TIME_UNSET); + assertExpectedWindow( + timeline, + calculateExpectedWindow( + /* availabilityStartTimeUs= */ 0L, + /* liveWindowDurationUs= */ 60_000_000L, + /* nowUs= */ 60_000_123L, + adSequencePattern), + adSequencePattern); + + // Advance nowUs so that the window ends just 1us before the next period moves into the window. + timeline.advanceNowUs(19999877L); + + // Assert that an additional period has not been included in the live window. + assertThat(timeline.getPeriodCount()).isEqualTo(4); + assertThat(timeline.getWindow(0, window).windowStartTimeMs).isEqualTo(20_000L); + assertThat(timeline.getPeriod(0, period).id).isEqualTo(0); + assertThat(timeline.getPeriod(0, period).uid).isEqualTo("uid-0[c]"); + assertThat(timeline.getPeriod(0, period).positionInWindowUs).isEqualTo(-20_000_000L); + assertThat(timeline.getPeriod(1, period).positionInWindowUs).isEqualTo(10_000_000L); + assertThat(timeline.getPeriod(2, period).positionInWindowUs).isEqualTo(20_000_000L); + assertThat(timeline.getPeriod(3, period).positionInWindowUs).isEqualTo(30_000_000L); + assertThat(timeline.getPeriod(3, period).durationUs).isEqualTo(C.TIME_UNSET); + assertExpectedWindow( + timeline, + calculateExpectedWindow( + /* availabilityStartTimeUs= */ 0L, + /* liveWindowDurationUs= */ 60_000_000L, + /* nowUs= */ 60_000_123L + 19999877L, + adSequencePattern), + adSequencePattern); + + // Advance the window by 1us to add the next period at the end of the window. + timeline.advanceNowUs(1L); + + // Assert that the previously first period has been moved out of the live window. + assertThat(timeline.getPeriodCount()).isEqualTo(5); + assertThat(timeline.getWindow(0, window).windowStartTimeMs).isEqualTo(20_000L); + assertThat(timeline.getPeriod(0, period).id).isEqualTo(0); + assertThat(timeline.getPeriod(0, period).uid).isEqualTo("uid-0[c]"); + assertThat(timeline.getPeriod(0, period).positionInWindowUs).isEqualTo(-20_000_001L); + assertThat(timeline.getPeriod(1, period).positionInWindowUs).isEqualTo(9_999_999L); + assertThat(timeline.getPeriod(2, period).positionInWindowUs).isEqualTo(19_999_999L); + assertThat(timeline.getPeriod(3, period).positionInWindowUs).isEqualTo(29_999_999L); + assertThat(timeline.getPeriod(4, period).positionInWindowUs).isEqualTo(59_999_999L); + assertThat(timeline.getPeriod(4, period).durationUs).isEqualTo(C.TIME_UNSET); + assertExpectedWindow( + timeline, + calculateExpectedWindow( + /* availabilityStartTimeUs= */ 0L, + /* liveWindowDurationUs= */ 60_000_000L, + /* nowUs= */ 60_000_123L + 19999878L, + adSequencePattern), + adSequencePattern); + } + + @Test + public void newInstance_advancedAvailabilityStartTime_correctlyInterpolatedPeriodIds() { + Timeline.Period period = new Timeline.Period(); + long availabilityStartTimeUs = 0; + long nowUs = 120_000_123; + long liveWindowDurationUs = 60_000_987L; + boolean[] adSequencePattern = {false, true, true}; + long sequenceDurationUs = calculateAdSequencePatternDurationUs(adSequencePattern); + + FakeMultiPeriodLiveTimeline timeline = + new FakeMultiPeriodLiveTimeline( + availabilityStartTimeUs, liveWindowDurationUs, nowUs, adSequencePattern); + + assertThat(timeline.getWindow(0, new Timeline.Window()).windowStartTimeMs) + .isEqualTo(Util.usToMs(nowUs - liveWindowDurationUs)); + assertThat(timeline.getPeriodCount()).isEqualTo(4); + assertThat(timeline.getPeriod(/* periodIndex= */ 0, period).id).isEqualTo(3); + assertThat(timeline.getPeriod(/* periodIndex= */ 0, period).uid).isEqualTo("uid-3[c]"); + assertThat(timeline.getPeriod(/* periodIndex= */ 1, period).id).isEqualTo(4); + assertThat(timeline.getPeriod(/* periodIndex= */ 1, period).uid).isEqualTo("uid-4[a]"); + assertThat(timeline.getPeriod(/* periodIndex= */ 2, period).id).isEqualTo(5); + assertThat(timeline.getPeriod(/* periodIndex= */ 2, period).uid).isEqualTo("uid-5[a]"); + assertThat(timeline.getPeriod(/* periodIndex= */ 3, period).id).isEqualTo(6); + assertThat(timeline.getPeriod(/* periodIndex= */ 3, period).uid).isEqualTo("uid-6[c]"); + assertExpectedWindow( + timeline, + calculateExpectedWindow( + availabilityStartTimeUs, liveWindowDurationUs, nowUs, adSequencePattern), + adSequencePattern); + + timeline.advanceNowUs(sequenceDurationUs * 13); + + assertThat(timeline.getWindow(0, new Timeline.Window()).windowStartTimeMs) + .isEqualTo(Util.usToMs((nowUs + sequenceDurationUs * 13) - liveWindowDurationUs)); + assertThat(timeline.getPeriodCount()).isEqualTo(4); + assertThat(timeline.getPeriod(/* periodIndex= */ 0, period).id).isEqualTo((13 * 3) + 3); + assertThat(timeline.getPeriod(/* periodIndex= */ 0, period).uid) + .isEqualTo("uid-" + ((13 * 3) + 3) + "[c]"); + assertThat(timeline.getPeriod(/* periodIndex= */ 1, period).id).isEqualTo((13 * 3) + 4); + assertThat(timeline.getPeriod(/* periodIndex= */ 1, period).uid) + .isEqualTo("uid-" + ((13 * 3) + 4) + "[a]"); + assertThat(timeline.getPeriod(/* periodIndex= */ 2, period).id).isEqualTo((13 * 3) + 5); + assertThat(timeline.getPeriod(/* periodIndex= */ 2, period).uid) + .isEqualTo("uid-" + ((13 * 3) + 5) + "[a]"); + assertThat(timeline.getPeriod(/* periodIndex= */ 3, period).id).isEqualTo((13 * 3) + 6); + assertThat(timeline.getPeriod(/* periodIndex= */ 3, period).uid) + .isEqualTo("uid-" + ((13 * 3) + 6) + "[c]"); + assertExpectedWindow( + timeline, + calculateExpectedWindow( + availabilityStartTimeUs, + liveWindowDurationUs, + (nowUs + sequenceDurationUs * 13), + adSequencePattern), + adSequencePattern); + } + + @Test + public void newInstance_availabilitySinceAWeekAfterStartOfUnixEpoch_correctLiveWindow() { + long availabilityStartTimeUs = 7 * A_DAY_US; + long nowUs = 18 * A_DAY_US + 135_000_000; + long liveWindowDurationUs = 60_000_000L; + boolean[] adSequencePattern = {false, true, true}; + + FakeMultiPeriodLiveTimeline timeline = + new FakeMultiPeriodLiveTimeline( + availabilityStartTimeUs, liveWindowDurationUs, nowUs, adSequencePattern); + + assertThat(timeline.getWindow(0, new Timeline.Window()).windowStartTimeMs) + .isEqualTo(Util.usToMs(nowUs - liveWindowDurationUs)); + assertExpectedWindow( + timeline, + calculateExpectedWindow( + availabilityStartTimeUs, liveWindowDurationUs, nowUs, adSequencePattern), + adSequencePattern); + } + + @Test + public void newInstance_adSequencePattern_correctPeriodTypesFromStartOfAvailability() { + FakeMultiPeriodLiveTimeline timeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0L, + /* liveWindowDurationUs= */ 120_000_000L, + /* nowUs= */ 120_000_000L, + new boolean[] {false, true, true, true}); + Timeline.Period period = new Timeline.Period(); + Timeline.Window window = new Timeline.Window(); + + assertThat(timeline.getPeriodCount()).isEqualTo(8); + assertThat(timeline.getWindow(0, window).windowStartTimeMs).isEqualTo(0L); + assertThat(timeline.getPeriod(0, period).positionInWindowUs).isEqualTo(0L); + assertThat(timeline.getPeriod(0, period).uid).isEqualTo("uid-0[c]"); + assertThat(timeline.getPeriod(1, period).uid).isEqualTo("uid-1[a]"); + assertThat(timeline.getPeriod(2, period).uid).isEqualTo("uid-2[a]"); + assertThat(timeline.getPeriod(3, period).uid).isEqualTo("uid-3[a]"); + assertThat(timeline.getPeriod(4, period).uid).isEqualTo("uid-4[c]"); + assertThat(timeline.getPeriod(5, period).uid).isEqualTo("uid-5[a]"); + assertThat(timeline.getPeriod(6, period).uid).isEqualTo("uid-6[a]"); + assertThat(timeline.getPeriod(7, period).uid).isEqualTo("uid-7[a]"); + + timeline.advanceNowUs(40_000_000L); + + assertThat(timeline.getPeriodCount()).isEqualTo(8); + assertThat(timeline.getWindow(0, window).windowStartTimeMs).isEqualTo(40_000L); + assertThat(timeline.getPeriod(0, period).positionInWindowUs).isEqualTo(0L); + assertThat(timeline.getPeriod(0, period).uid).isEqualTo("uid-2[a]"); + assertThat(timeline.getPeriod(1, period).uid).isEqualTo("uid-3[a]"); + assertThat(timeline.getPeriod(2, period).uid).isEqualTo("uid-4[c]"); + assertThat(timeline.getPeriod(3, period).uid).isEqualTo("uid-5[a]"); + assertThat(timeline.getPeriod(4, period).uid).isEqualTo("uid-6[a]"); + assertThat(timeline.getPeriod(5, period).uid).isEqualTo("uid-7[a]"); + assertThat(timeline.getPeriod(6, period).uid).isEqualTo("uid-8[c]"); + assertThat(timeline.getPeriod(7, period).uid).isEqualTo("uid-9[a]"); + + timeline = + new FakeMultiPeriodLiveTimeline( + /* availabilityStartTimeUs= */ 0L, + /* liveWindowDurationUs= */ 220_000_000L, + /* nowUs= */ 250_000_000L, + new boolean[] {false, true, false, true, false}); + + assertThat(timeline.getPeriodCount()).isEqualTo(10); + assertThat(timeline.getWindow(0, window).windowStartTimeMs).isEqualTo(30_000L); + assertThat(timeline.getPeriod(0, period).positionInWindowUs).isEqualTo(0L); + assertThat(timeline.getPeriod(0, period).uid).isEqualTo("uid-1[a]"); + assertThat(timeline.getPeriod(1, period).uid).isEqualTo("uid-2[c]"); + assertThat(timeline.getPeriod(2, period).uid).isEqualTo("uid-3[a]"); + assertThat(timeline.getPeriod(3, period).uid).isEqualTo("uid-4[c]"); + assertThat(timeline.getPeriod(4, period).uid).isEqualTo("uid-5[c]"); + assertThat(timeline.getPeriod(5, period).uid).isEqualTo("uid-6[a]"); + assertThat(timeline.getPeriod(6, period).uid).isEqualTo("uid-7[c]"); + assertThat(timeline.getPeriod(7, period).uid).isEqualTo("uid-8[a]"); + assertThat(timeline.getPeriod(8, period).uid).isEqualTo("uid-9[c]"); + assertThat(timeline.getPeriod(9, period).uid).isEqualTo("uid-10[c]"); + } + + private ExpectedWindow calculateExpectedWindow( + long availabilityStartTimeUs, + long liveWindowDurationUs, + long nowUs, + boolean[] adSequencePattern) { + long windowStartTimeUs = nowUs - liveWindowDurationUs; + long sequenceDurationUs = calculateAdSequencePatternDurationUs(adSequencePattern); + long durationBeforeWindowStartUs = windowStartTimeUs - availabilityStartTimeUs; + long skippedSequenceCount = durationBeforeWindowStartUs / sequenceDurationUs; + long remainingDurationBeforeWindowUs = durationBeforeWindowStartUs % sequenceDurationUs; + int idOfFirstPeriodInWindow = (int) (skippedSequenceCount * adSequencePattern.length); + long lastSkippedPeriodDurationUs = 0L; + // Skip period by period until we reach the window start. + while (remainingDurationBeforeWindowUs > 0) { + boolean isAd = adSequencePattern[idOfFirstPeriodInWindow++ % adSequencePattern.length]; + lastSkippedPeriodDurationUs = isAd ? AD_PERIOD_DURATION_US : PERIOD_DURATION_US; + remainingDurationBeforeWindowUs -= lastSkippedPeriodDurationUs; + } + long positionOfFirstPeriodInWindowUs = 0; + if (remainingDurationBeforeWindowUs < 0) { + // The previous period overlaps into the window, so the window starts in the previous period. + idOfFirstPeriodInWindow--; + // The negative duration of the part of the period that is not overlapping the window. + positionOfFirstPeriodInWindowUs = + -(lastSkippedPeriodDurationUs + remainingDurationBeforeWindowUs); + } + long durationOfFirstPeriodInWindowUs = + adSequencePattern[idOfFirstPeriodInWindow % adSequencePattern.length] + ? AD_PERIOD_DURATION_US + : PERIOD_DURATION_US; + long durationInWindowUs = + remainingDurationBeforeWindowUs == 0 + ? durationOfFirstPeriodInWindowUs + : -remainingDurationBeforeWindowUs; + int idOfLastPeriodInWindow = idOfFirstPeriodInWindow; + while (durationInWindowUs < liveWindowDurationUs) { + boolean isAd = adSequencePattern[++idOfLastPeriodInWindow % adSequencePattern.length]; + durationInWindowUs += isAd ? AD_PERIOD_DURATION_US : PERIOD_DURATION_US; + } + return new ExpectedWindow( + idOfFirstPeriodInWindow, idOfLastPeriodInWindow, positionOfFirstPeriodInWindowUs); + } + + private void assertExpectedWindow( + Timeline timeline, ExpectedWindow expectedWindow, boolean[] adSequencePattern) { + Timeline.Period period = new Timeline.Period(); + assertThat(timeline.getPeriodCount()) + .isEqualTo(expectedWindow.idOfLastPeriod - expectedWindow.idOfFirstPeriod + 1); + long positionInWindowUs = expectedWindow.positionOfFirstPeriodInWindowUs; + for (int i = 0; i < timeline.getPeriodCount(); i++) { + int id = expectedWindow.idOfFirstPeriod + i; + boolean isAd = adSequencePattern[id % adSequencePattern.length]; + assertThat(timeline.getPeriod(i, period).id).isEqualTo(id); + assertThat(timeline.getPeriod(i, period).uid) + .isEqualTo("uid-" + id + "[" + (isAd ? "a" : "c") + "]"); + assertThat(timeline.getPeriod(i, period).positionInWindowUs).isEqualTo(positionInWindowUs); + positionInWindowUs += isAd ? AD_PERIOD_DURATION_US : PERIOD_DURATION_US; + } + } + + private static class ExpectedWindow { + + private final int idOfFirstPeriod; + private final int idOfLastPeriod; + private final long positionOfFirstPeriodInWindowUs; + + /** Creates an instance. */ + public ExpectedWindow( + int idOfFirstPeriod, int idOfLastPeriod, long positionOfFirstPeriodInWindowUs) { + this.idOfFirstPeriod = idOfFirstPeriod; + this.idOfLastPeriod = idOfLastPeriod; + this.positionOfFirstPeriodInWindowUs = positionOfFirstPeriodInWindowUs; + } + } +}