Back to Repositories

Testing ClippingMediaSource Timeline Management in SmartTube

A comprehensive test suite for ClippingMediaSource in ExoPlayer that verifies proper media timeline clipping functionality and event handling. The tests cover clipping ranges, dynamic updates, and media loading behavior.

Test Coverage Overview

The test suite provides extensive coverage of ClippingMediaSource functionality including:
  • Basic timeline clipping with various start/end ranges
  • Handling of seekable/unseekable windows
  • Dynamic timeline updates with overlapping and non-overlapping live windows
  • Event timing and media load data handling
  • Edge cases like unset durations and clipping from default positions

Implementation Analysis

The testing approach uses JUnit and Robolectric for Android environment simulation. Tests are structured around the FakeMediaSource and MediaSourceTestRunner classes to validate timeline transformations and event propagation. Key patterns include timeline window/period validation and media load data verification.

Technical Details

Testing tools and configuration:
  • JUnit 4 with AndroidJUnit4 runner
  • Robolectric for Android framework simulation
  • Custom test utilities: FakeMediaSource, MediaSourceTestRunner, TimelineAsserts
  • Timeline and Period/Window validation helpers

Best Practices Demonstrated

The test suite demonstrates several testing best practices:
  • Comprehensive edge case coverage
  • Proper test isolation and resource cleanup
  • Clear test method naming and organization
  • Effective use of test utilities and helpers
  • Thorough validation of both happy path and error scenarios

yuliskov/smarttube

exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java

            
/*
 * Copyright (C) 2016 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 com.google.android.exoplayer2.source;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;

import android.os.Handler;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.Timeline.Window;
import com.google.android.exoplayer2.source.ClippingMediaSource.IllegalClippingException;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;
import com.google.android.exoplayer2.testutil.FakeMediaPeriod;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener;
import java.io.IOException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;

/** Unit tests for {@link ClippingMediaSource}. */
@RunWith(AndroidJUnit4.class)
@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})
public final class ClippingMediaSourceTest {

  private static final long TEST_PERIOD_DURATION_US = 1000000;
  private static final long TEST_CLIP_AMOUNT_US = 300000;

  private Window window;
  private Period period;

  @Before
  public void setUp() throws Exception {
    window = new Timeline.Window();
    period = new Timeline.Period();
  }

  @Test
  public void testNoClipping() throws IOException {
    Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false);

    Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US);

    assertThat(clippedTimeline.getWindowCount()).isEqualTo(1);
    assertThat(clippedTimeline.getPeriodCount()).isEqualTo(1);
    assertThat(clippedTimeline.getWindow(0, window).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US);
    assertThat(clippedTimeline.getPeriod(0, period).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US);
  }

  @Test
  public void testClippingUnseekableWindowThrows() throws IOException {
    Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, false, false);

    // If the unseekable window isn't clipped, clipping succeeds.
    getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US);
    try {
      // If the unseekable window is clipped, clipping fails.
      getClippedTimeline(timeline, 1, TEST_PERIOD_DURATION_US);
      fail("Expected clipping to fail.");
    } catch (IllegalClippingException e) {
      assertThat(e.reason).isEqualTo(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START);
    }
  }

  @Test
  public void testClippingStart() throws IOException {
    Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false);

    Timeline clippedTimeline =
        getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US);
    assertThat(clippedTimeline.getWindow(0, window).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimeline.getPeriod(0, period).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US);
  }

  @Test
  public void testClippingEnd() throws IOException {
    Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false);

    Timeline clippedTimeline =
        getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimeline.getWindow(0, window).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimeline.getPeriod(0, period).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
  }

  @Test
  public void testClippingStartAndEndInitial() throws IOException {
    // Timeline that's dynamic and not seekable. A child source might report such a timeline prior
    // to it having loaded sufficient data to establish its duration and seekability. Such timelines
    // should not result in clipping failure.
    Timeline timeline =
        new SinglePeriodTimeline(C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ true);

    Timeline clippedTimeline =
        getClippedTimeline(
            timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2);
    assertThat(clippedTimeline.getWindow(0, window).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3);
    assertThat(clippedTimeline.getPeriod(0, period).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2);
  }

  @Test
  public void testClippingToEndOfSourceWithDurationSetsDuration() throws IOException {
    // Create a child timeline that has a known duration.
    Timeline timeline =
        new SinglePeriodTimeline(
            /* durationUs= */ TEST_PERIOD_DURATION_US,
            /* isSeekable= */ true,
            /* isDynamic= */ false);

    // When clipping to the end, the clipped timeline should also have a duration.
    Timeline clippedTimeline =
        getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, C.TIME_END_OF_SOURCE);
    assertThat(clippedTimeline.getWindow(/* windowIndex= */ 0, window).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
  }

  @Test
  public void testClippingToEndOfSourceWithUnsetDurationDoesNotSetDuration() throws IOException {
    // Create a child timeline that has an unknown duration.
    Timeline timeline =
        new SinglePeriodTimeline(
            /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ true, /* isDynamic= */ false);

    // When clipping to the end, the clipped timeline should also have an unset duration.
    Timeline clippedTimeline =
        getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, C.TIME_END_OF_SOURCE);
    assertThat(clippedTimeline.getWindow(/* windowIndex= */ 0, window).getDurationUs())
        .isEqualTo(C.TIME_UNSET);
  }

  @Test
  public void testClippingStartAndEnd() throws IOException {
    Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false);

    Timeline clippedTimeline =
        getClippedTimeline(
            timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2);
    assertThat(clippedTimeline.getWindow(0, window).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3);
    assertThat(clippedTimeline.getPeriod(0, period).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2);
  }

  @Test
  public void testClippingFromDefaultPosition() throws IOException {
    Timeline timeline =
        new SinglePeriodTimeline(
            /* periodDurationUs= */ 3 * TEST_PERIOD_DURATION_US,
            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,
            /* windowPositionInPeriodUs= */ TEST_PERIOD_DURATION_US,
            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,
            /* isSeekable= */ true,
            /* isDynamic= */ true,
            /* tag= */ null);

    Timeline clippedTimeline = getClippedTimeline(timeline, /* durationUs= */ TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimeline.getWindow(0, window).getDurationUs()).isEqualTo(TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimeline.getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);
    assertThat(clippedTimeline.getWindow(0, window).getPositionInFirstPeriodUs())
        .isEqualTo(TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimeline.getPeriod(0, period).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US + 2 * TEST_CLIP_AMOUNT_US);
  }

  @Test
  public void testAllowDynamicUpdatesWithOverlappingLiveWindow() throws IOException {
    Timeline timeline1 =
        new SinglePeriodTimeline(
            /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US,
            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,
            /* windowPositionInPeriodUs= */ TEST_PERIOD_DURATION_US,
            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,
            /* isSeekable= */ true,
            /* isDynamic= */ true,
            /* tag= */ null);
    Timeline timeline2 =
        new SinglePeriodTimeline(
            /* periodDurationUs= */ 3 * TEST_PERIOD_DURATION_US,
            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,
            /* windowPositionInPeriodUs= */ 2 * TEST_PERIOD_DURATION_US,
            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,
            /* isSeekable= */ true,
            /* isDynamic= */ true,
            /* tag= */ null);

    Timeline[] clippedTimelines =
        getClippedTimelines(
            /* startUs= */ 0,
            /* endUs= */ TEST_PERIOD_DURATION_US,
            /* allowDynamicUpdates= */ true,
            /* fromDefaultPosition= */ true,
            timeline1,
            timeline2);
    assertThat(clippedTimelines[0].getWindow(0, window).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimelines[0].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);
    assertThat(clippedTimelines[0].getWindow(0, window).isDynamic).isTrue();
    assertThat(clippedTimelines[0].getWindow(0, window).getPositionInFirstPeriodUs())
        .isEqualTo(TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimelines[0].getPeriod(0, period).getDurationUs())
        .isEqualTo(2 * TEST_PERIOD_DURATION_US);
    assertThat(clippedTimelines[1].getWindow(0, window).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimelines[1].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);
    assertThat(clippedTimelines[1].getWindow(0, window).isDynamic).isTrue();
    assertThat(clippedTimelines[1].getWindow(0, window).getPositionInFirstPeriodUs())
        .isEqualTo(2 * TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimelines[1].getPeriod(0, period).getDurationUs())
        .isEqualTo(3 * TEST_PERIOD_DURATION_US);
  }

  @Test
  public void testAllowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOException {
    Timeline timeline1 =
        new SinglePeriodTimeline(
            /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US,
            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,
            /* windowPositionInPeriodUs= */ TEST_PERIOD_DURATION_US,
            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,
            /* isSeekable= */ true,
            /* isDynamic= */ true,
            /* tag= */ null);
    Timeline timeline2 =
        new SinglePeriodTimeline(
            /* periodDurationUs= */ 4 * TEST_PERIOD_DURATION_US,
            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,
            /* windowPositionInPeriodUs= */ 3 * TEST_PERIOD_DURATION_US,
            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,
            /* isSeekable= */ true,
            /* isDynamic= */ true,
            /* tag= */ null);

    Timeline[] clippedTimelines =
        getClippedTimelines(
            /* startUs= */ 0,
            /* endUs= */ TEST_PERIOD_DURATION_US,
            /* allowDynamicUpdates= */ true,
            /* fromDefaultPosition= */ true,
            timeline1,
            timeline2);
    assertThat(clippedTimelines[0].getWindow(0, window).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimelines[0].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);
    assertThat(clippedTimelines[0].getWindow(0, window).isDynamic).isTrue();
    assertThat(clippedTimelines[0].getWindow(0, window).getPositionInFirstPeriodUs())
        .isEqualTo(TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimelines[0].getPeriod(0, period).getDurationUs())
        .isEqualTo(2 * TEST_PERIOD_DURATION_US);
    assertThat(clippedTimelines[1].getWindow(0, window).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimelines[1].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);
    assertThat(clippedTimelines[1].getWindow(0, window).isDynamic).isTrue();
    assertThat(clippedTimelines[1].getWindow(0, window).getPositionInFirstPeriodUs())
        .isEqualTo(3 * TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimelines[1].getPeriod(0, period).getDurationUs())
        .isEqualTo(4 * TEST_PERIOD_DURATION_US);
  }

  @Test
  public void testDisallowDynamicUpdatesWithOverlappingLiveWindow() throws IOException {
    Timeline timeline1 =
        new SinglePeriodTimeline(
            /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US,
            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,
            /* windowPositionInPeriodUs= */ TEST_PERIOD_DURATION_US,
            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,
            /* isSeekable= */ true,
            /* isDynamic= */ true,
            /* tag= */ null);
    Timeline timeline2 =
        new SinglePeriodTimeline(
            /* periodDurationUs= */ 3 * TEST_PERIOD_DURATION_US,
            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,
            /* windowPositionInPeriodUs= */ 2 * TEST_PERIOD_DURATION_US,
            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,
            /* isSeekable= */ true,
            /* isDynamic= */ true,
            /* tag= */ null);

    Timeline[] clippedTimelines =
        getClippedTimelines(
            /* startUs= */ 0,
            /* endUs= */ TEST_PERIOD_DURATION_US,
            /* allowDynamicUpdates= */ false,
            /* fromDefaultPosition= */ true,
            timeline1,
            timeline2);
    assertThat(clippedTimelines[0].getWindow(0, window).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimelines[0].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);
    assertThat(clippedTimelines[0].getWindow(0, window).isDynamic).isTrue();
    assertThat(clippedTimelines[0].getWindow(0, window).getPositionInFirstPeriodUs())
        .isEqualTo(TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimelines[0].getPeriod(0, period).getDurationUs())
        .isEqualTo(2 * TEST_PERIOD_DURATION_US);
    assertThat(clippedTimelines[1].getWindow(0, window).getDurationUs())
        .isEqualTo(TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimelines[1].getWindow(0, window).getDefaultPositionUs())
        .isEqualTo(TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimelines[1].getWindow(0, window).isDynamic).isFalse();
    assertThat(clippedTimelines[1].getWindow(0, window).getPositionInFirstPeriodUs())
        .isEqualTo(2 * TEST_PERIOD_DURATION_US);
    assertThat(clippedTimelines[1].getPeriod(0, period).getDurationUs())
        .isEqualTo(2 * TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);
  }

  @Test
  public void testDisallowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOException {
    Timeline timeline1 =
        new SinglePeriodTimeline(
            /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US,
            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,
            /* windowPositionInPeriodUs= */ TEST_PERIOD_DURATION_US,
            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,
            /* isSeekable= */ true,
            /* isDynamic= */ true,
            /* tag= */ null);
    Timeline timeline2 =
        new SinglePeriodTimeline(
            /* periodDurationUs= */ 4 * TEST_PERIOD_DURATION_US,
            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,
            /* windowPositionInPeriodUs= */ 3 * TEST_PERIOD_DURATION_US,
            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,
            /* isSeekable= */ true,
            /* isDynamic= */ true,
            /* tag= */ null);

    Timeline[] clippedTimelines =
        getClippedTimelines(
            /* startUs= */ 0,
            /* endUs= */ TEST_PERIOD_DURATION_US,
            /* allowDynamicUpdates= */ false,
            /* fromDefaultPosition= */ true,
            timeline1,
            timeline2);
    assertThat(clippedTimelines[0].getWindow(0, window).getDurationUs())
        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimelines[0].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);
    assertThat(clippedTimelines[0].getWindow(0, window).isDynamic).isTrue();
    assertThat(clippedTimelines[0].getWindow(0, window).getPositionInFirstPeriodUs())
        .isEqualTo(TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);
    assertThat(clippedTimelines[0].getPeriod(0, period).getDurationUs())
        .isEqualTo(2 * TEST_PERIOD_DURATION_US);
    assertThat(clippedTimelines[1].getWindow(0, window).getDurationUs()).isEqualTo(0);
    assertThat(clippedTimelines[1].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);
    assertThat(clippedTimelines[1].getWindow(0, window).isDynamic).isFalse();
    assertThat(clippedTimelines[1].getWindow(0, window).getPositionInFirstPeriodUs())
        .isEqualTo(3 * TEST_PERIOD_DURATION_US);
    assertThat(clippedTimelines[1].getPeriod(0, period).getDurationUs())
        .isEqualTo(3 * TEST_PERIOD_DURATION_US);
  }

  @Test
  public void testWindowAndPeriodIndices() throws IOException {
    Timeline timeline =
        new FakeTimeline(
            new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US));
    Timeline clippedTimeline =
        getClippedTimeline(
            timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
    TimelineAsserts.assertWindowTags(clippedTimeline, 111);
    TimelineAsserts.assertPeriodCounts(clippedTimeline, 1);
    TimelineAsserts.assertPreviousWindowIndices(
        clippedTimeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET);
    TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, false, 0);
    TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0);
    TimelineAsserts.assertNextWindowIndices(
        clippedTimeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET);
    TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, false, 0);
    TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0);
  }

  @Test
  public void testEventTimeWithinClippedRange() throws IOException {
    MediaLoadData mediaLoadData =
        getClippingMediaSourceMediaLoadData(
            /* clippingStartUs= */ TEST_CLIP_AMOUNT_US,
            /* clippingEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
            /* eventStartUs= */ TEST_CLIP_AMOUNT_US + 1000,
            /* eventEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US - 1000);
    assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(1000);
    assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs))
        .isEqualTo(TEST_PERIOD_DURATION_US - 2 * TEST_CLIP_AMOUNT_US - 1000);
  }

  @Test
  public void testEventTimeOutsideClippedRange() throws IOException {
    MediaLoadData mediaLoadData =
        getClippingMediaSourceMediaLoadData(
            /* clippingStartUs= */ TEST_CLIP_AMOUNT_US,
            /* clippingEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
            /* eventStartUs= */ TEST_CLIP_AMOUNT_US - 1000,
            /* eventEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US + 1000);
    assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(0);
    assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs))
        .isEqualTo(TEST_PERIOD_DURATION_US - 2 * TEST_CLIP_AMOUNT_US);
  }

  @Test
  public void testUnsetEventTime() throws IOException {
    MediaLoadData mediaLoadData =
        getClippingMediaSourceMediaLoadData(
            /* clippingStartUs= */ TEST_CLIP_AMOUNT_US,
            /* clippingEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
            /* eventStartUs= */ C.TIME_UNSET,
            /* eventEndUs= */ C.TIME_UNSET);
    assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(C.TIME_UNSET);
    assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs)).isEqualTo(C.TIME_UNSET);
  }

  @Test
  public void testEventTimeWithUnsetDuration() throws IOException {
    MediaLoadData mediaLoadData =
        getClippingMediaSourceMediaLoadData(
            /* clippingStartUs= */ TEST_CLIP_AMOUNT_US,
            /* clippingEndUs= */ C.TIME_END_OF_SOURCE,
            /* eventStartUs= */ TEST_CLIP_AMOUNT_US,
            /* eventEndUs= */ TEST_CLIP_AMOUNT_US + 1_000_000);
    assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(0);
    assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs)).isEqualTo(1_000_000);
  }

  /**
   * Wraps a timeline of duration {@link #TEST_PERIOD_DURATION_US} in a {@link ClippingMediaSource},
   * sends a media source event from the child source and returns the reported {@link MediaLoadData}
   * for the clipping media source.
   *
   * @param clippingStartUs The start time of the media source clipping, in microseconds.
   * @param clippingEndUs The end time of the media source clipping, in microseconds.
   * @param eventStartUs The start time of the media source event (before clipping), in
   *     microseconds.
   * @param eventEndUs The end time of the media source event (before clipping), in microseconds.
   * @return The reported {@link MediaLoadData} for that event.
   */
  private static MediaLoadData getClippingMediaSourceMediaLoadData(
      long clippingStartUs, long clippingEndUs, final long eventStartUs, final long eventEndUs)
      throws IOException {
    Timeline timeline =
        new SinglePeriodTimeline(
            TEST_PERIOD_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false);
    FakeMediaSource fakeMediaSource =
        new FakeMediaSource(timeline, /* manifest= */ null) {
          @Override
          protected FakeMediaPeriod createFakeMediaPeriod(
              MediaPeriodId id,
              TrackGroupArray trackGroupArray,
              Allocator allocator,
              EventDispatcher eventDispatcher,
              @Nullable TransferListener transferListener) {
            eventDispatcher.downstreamFormatChanged(
                new MediaLoadData(
                    C.DATA_TYPE_MEDIA,
                    C.TRACK_TYPE_UNKNOWN,
                    /* trackFormat= */ null,
                    C.SELECTION_REASON_UNKNOWN,
                    /* trackSelectionData= */ null,
                    C.usToMs(eventStartUs),
                    C.usToMs(eventEndUs)));
            return super.createFakeMediaPeriod(
                id, trackGroupArray, allocator, eventDispatcher, transferListener);
          }
        };
    final ClippingMediaSource clippingMediaSource =
        new ClippingMediaSource(fakeMediaSource, clippingStartUs, clippingEndUs);
    MediaSourceTestRunner testRunner =
        new MediaSourceTestRunner(clippingMediaSource, /* allocator= */ null);
    final MediaLoadData[] reportedMediaLoadData = new MediaLoadData[1];
    try {
      testRunner.runOnPlaybackThread(
          () ->
              clippingMediaSource.addEventListener(
                  new Handler(),
                  new DefaultMediaSourceEventListener() {
                    @Override
                    public void onDownstreamFormatChanged(
                        int windowIndex,
                        @Nullable MediaPeriodId mediaPeriodId,
                        MediaLoadData mediaLoadData) {
                      reportedMediaLoadData[0] = mediaLoadData;
                    }
                  }));
      testRunner.prepareSource();
      // Create period to send the test event configured above.
      testRunner.createPeriod(
          new MediaPeriodId(
              timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
      assertThat(reportedMediaLoadData[0]).isNotNull();
    } finally {
      testRunner.release();
    }
    return reportedMediaLoadData[0];
  }

  /**
   * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
   */
  private static Timeline getClippedTimeline(Timeline timeline, long startUs, long endUs)
      throws IOException {
    FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, /* manifest= */ null);
    ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startUs, endUs);
    return getClippedTimelines(fakeMediaSource, mediaSource)[0];
  }

  /**
   * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
   */
  private static Timeline getClippedTimeline(Timeline timeline, long durationUs)
      throws IOException {
    FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, /* manifest= */ null);
    ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, durationUs);
    return getClippedTimelines(fakeMediaSource, mediaSource)[0];
  }

  /**
   * Wraps the specified timelines in a {@link ClippingMediaSource} and returns the clipped timeline
   * for each timeline update.
   */
  private static Timeline[] getClippedTimelines(
      long startUs,
      long endUs,
      boolean allowDynamicUpdates,
      boolean fromDefaultPosition,
      Timeline firstTimeline,
      Timeline... additionalTimelines)
      throws IOException {
    FakeMediaSource fakeMediaSource = new FakeMediaSource(firstTimeline, /* manifest= */ null);
    ClippingMediaSource mediaSource =
        new ClippingMediaSource(
            fakeMediaSource,
            startUs,
            endUs,
            /* enableInitialDiscontinuity= */ true,
            allowDynamicUpdates,
            fromDefaultPosition);
    return getClippedTimelines(fakeMediaSource, mediaSource, additionalTimelines);
  }

  private static Timeline[] getClippedTimelines(
      FakeMediaSource fakeMediaSource,
      ClippingMediaSource clippingMediaSource,
      Timeline... additionalTimelines)
      throws IOException {
    MediaSourceTestRunner testRunner =
        new MediaSourceTestRunner(clippingMediaSource, /* allocator= */ null);
    Timeline[] clippedTimelines = new Timeline[additionalTimelines.length + 1];
    try {
      clippedTimelines[0] = testRunner.prepareSource();
      MediaPeriod mediaPeriod =
          testRunner.createPeriod(
              new MediaPeriodId(
                  clippedTimelines[0].getUidOfPeriod(/* periodIndex= */ 0),
                  /* windowSequenceNumber= */ 0));
      for (int i = 0; i < additionalTimelines.length; i++) {
        fakeMediaSource.setNewSourceInfo(additionalTimelines[i], /* newManifest= */ null);
        clippedTimelines[i + 1] = testRunner.assertTimelineChangeBlocking();
      }
      testRunner.releasePeriod(mediaPeriod);
      testRunner.releaseSource();
      fakeMediaSource.assertReleased();
      return clippedTimelines;
    } finally {
      testRunner.release();
    }
  }
}