From df575a8d1909aa431dcf81a3275a5b1be86e8fd3 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 Jan 2025 09:20:03 -0800 Subject: [PATCH] Add ClippingMediaSource.Builder This prevents complicated constructor changes when we add new options. PiperOrigin-RevId: 721415339 --- .../exoplayer/source/ClippingMediaSource.java | 254 +++++++++++++----- .../source/DefaultMediaSourceFactory.java | 14 +- .../media3/exoplayer/ExoPlayerTest.java | 33 +-- .../e2etest/MergingPlaylistPlaybackTest.java | 12 +- .../source/ClippingMediaSourceTest.java | 40 +-- .../media3/transformer/CompositionPlayer.java | 8 +- 6 files changed, 245 insertions(+), 116 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ClippingMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ClippingMediaSource.java index 5d7bed757d..3955426006 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ClippingMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ClippingMediaSource.java @@ -15,7 +15,10 @@ */ package androidx.media3.exoplayer.source; +import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.Util.msToUs; import static java.lang.Math.max; import static java.lang.Math.min; import static java.lang.annotation.ElementType.TYPE_USE; @@ -29,6 +32,7 @@ import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.upstream.Allocator; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -43,6 +47,161 @@ import java.util.ArrayList; @UnstableApi public final class ClippingMediaSource extends WrappingMediaSource { + /** A builder for {@link ClippingMediaSource}. */ + public static final class Builder { + + private final MediaSource mediaSource; + + private long startPositionUs; + private long endPositionUs; + private boolean enableInitialDiscontinuity; + private boolean allowDynamicClippingUpdates; + private boolean relativeToDefaultPosition; + private boolean buildCalled; + + /** + * Creates the builder. + * + * @param mediaSource The {@link MediaSource} to clip. + */ + public Builder(MediaSource mediaSource) { + this.mediaSource = checkNotNull(mediaSource); + this.enableInitialDiscontinuity = true; + this.endPositionUs = C.TIME_END_OF_SOURCE; + } + + /** + * Sets the clip start position. + * + *

The start position is relative to the wrapped source's {@link Timeline.Window}, unless + * {@link #setRelativeToDefaultPosition} is set to {@code true}. + * + * @param startPositionMs The clip start position in milliseconds. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setStartPositionMs(long startPositionMs) { + return setStartPositionUs(msToUs(startPositionMs)); + } + + /** + * Sets the clip start position. + * + *

The start position is relative to the wrapped source's {@link Timeline.Window}, unless + * {@link #setRelativeToDefaultPosition} is set to {@code true}. + * + * @param startPositionUs The clip start position in microseconds. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setStartPositionUs(long startPositionUs) { + checkArgument(startPositionUs >= 0); + checkState(!buildCalled); + this.startPositionUs = startPositionUs; + return this; + } + + /** + * Sets the clip end position. + * + *

The end position is relative to the wrapped source's {@link Timeline.Window}, unless + * {@link #setRelativeToDefaultPosition} is set to {@code true}. + * + *

Specify {@link C#TIME_END_OF_SOURCE} to provide samples up to the end of the source. + * Specifying a position that exceeds the wrapped source's duration will also result in the end + * of the source not being clipped. + * + * @param endPositionMs The clip end position in milliseconds, or {@link C#TIME_END_OF_SOURCE}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setEndPositionMs(long endPositionMs) { + return setEndPositionUs(msToUs(endPositionMs)); + } + + /** + * Sets the clip end position. + * + *

The end position is relative to the wrapped source's {@link Timeline.Window}, unless + * {@link #setRelativeToDefaultPosition} is set to {@code true}. + * + *

Specify {@link C#TIME_END_OF_SOURCE} to provide samples up to the end of the source. + * Specifying a position that exceeds the wrapped source's duration will also result in the end + * of the source not being clipped. + * + * @param endPositionUs The clip end position in microseconds, or {@link C#TIME_END_OF_SOURCE}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setEndPositionUs(long endPositionUs) { + checkState(!buildCalled); + this.endPositionUs = endPositionUs; + return this; + } + + /** + * Sets whether to enable the initial discontinuity. + * + *

This discontinuity is needed to handle pre-rolling samples from a previous keyframe if the + * start position doesn't fall onto a keyframe. + * + *

When starting from the beginning of the stream or when clipping a format that is + * guaranteed to have keyframes only, the discontinuity won't be applied even if enabled. + * + *

The default value is {@code true}. + * + * @param enableInitialDiscontinuity Whether to enable the initial discontinuity. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setEnableInitialDiscontinuity(boolean enableInitialDiscontinuity) { + checkState(!buildCalled); + this.enableInitialDiscontinuity = enableInitialDiscontinuity; + return this; + } + + /** + * Sets whether the clipping of active media periods moves with a live window. + * + *

If {@code false}, playback ends when it reaches {@code endPositionUs} in the last reported + * live window at the time a media period was created. + * + *

The default value is {@code false}. + * + * @param allowDynamicClippingUpdates Whether to allow dynamic clipping updates. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setAllowDynamicClippingUpdates(boolean allowDynamicClippingUpdates) { + checkState(!buildCalled); + this.allowDynamicClippingUpdates = allowDynamicClippingUpdates; + return this; + } + + /** + * Sets whether the start and end position are relative to the default position of the wrapped + * source's {@link Timeline.Window}. + * + *

The default value is {@code false}. + * + * @param relativeToDefaultPosition Whether the start and end positions are relative to the + * default position of the wrapped source's {@link Timeline.Window}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setRelativeToDefaultPosition(boolean relativeToDefaultPosition) { + checkState(!buildCalled); + this.relativeToDefaultPosition = relativeToDefaultPosition; + return this; + } + + /** Builds the {@link ClippingMediaSource}. */ + public ClippingMediaSource build() { + buildCalled = true; + return new ClippingMediaSource(this); + } + } + /** Thrown when a {@link ClippingMediaSource} cannot clip its wrapped source. */ public static final class IllegalClippingException extends IOException { @@ -109,80 +268,28 @@ public final class ClippingMediaSource extends WrappingMediaSource { private long periodEndUs; /** - * Creates a new clipping source that wraps the specified source and provides samples between the - * specified start and end position. - * - * @param mediaSource The single-period source to wrap. - * @param startPositionUs The start position within {@code mediaSource}'s window at which to start - * providing samples, in microseconds. - * @param endPositionUs The end position within {@code mediaSource}'s window at which to stop - * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples - * from the specified start point up to the end of the source. Specifying a position that - * exceeds the {@code mediaSource}'s duration will also result in the end of the source not - * being clipped. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) { this( - mediaSource, - startPositionUs, - endPositionUs, - /* enableInitialDiscontinuity= */ true, - /* allowDynamicClippingUpdates= */ false, - /* relativeToDefaultPosition= */ false); + new Builder(mediaSource) + .setStartPositionUs(startPositionUs) + .setEndPositionUs(endPositionUs)); } /** - * Creates a new clipping source that wraps the specified source and provides samples from the - * default position for the specified duration. - * - * @param mediaSource The single-period source to wrap. - * @param durationUs The duration from the default position in the window in {@code mediaSource}'s - * timeline at which to stop providing samples. Specifying a duration that exceeds the {@code - * mediaSource}'s duration will result in the end of the source not being clipped. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public ClippingMediaSource(MediaSource mediaSource, long durationUs) { - this( - mediaSource, - /* startPositionUs= */ 0, - /* endPositionUs= */ durationUs, - /* enableInitialDiscontinuity= */ true, - /* allowDynamicClippingUpdates= */ false, - /* relativeToDefaultPosition= */ true); + this(new Builder(mediaSource).setEndPositionUs(durationUs).setRelativeToDefaultPosition(true)); } /** - * Creates a new clipping source that wraps the specified source. - * - *

If the start point is guaranteed to be a key frame, pass {@code false} to {@code - * enableInitialPositionDiscontinuity} to suppress an initial discontinuity when a period is first - * read from. - * - *

For live streams, if the clipping positions should move with the live window, pass {@code - * true} to {@code allowDynamicClippingUpdates}. Otherwise, the live stream ends when the playback - * reaches {@code endPositionUs} in the last reported live window at the time a media period was - * created. - * - * @param mediaSource The single-period source to wrap. - * @param startPositionUs The start position at which to start providing samples, in microseconds. - * If {@code relativeToDefaultPosition} is {@code false}, this position is relative to the - * start of the window in {@code mediaSource}'s timeline. If {@code relativeToDefaultPosition} - * is {@code true}, this position is relative to the default position in the window in {@code - * mediaSource}'s timeline. - * @param endPositionUs The end position at which to stop providing samples, in microseconds. - * Specify {@link C#TIME_END_OF_SOURCE} to provide samples from the specified start point up - * to the end of the source. Specifying a position that exceeds the {@code mediaSource}'s - * duration will also result in the end of the source not being clipped. If {@code - * relativeToDefaultPosition} is {@code false}, the specified position is relative to the - * start of the window in {@code mediaSource}'s timeline. If {@code relativeToDefaultPosition} - * is {@code true}, this position is relative to the default position in the window in {@code - * mediaSource}'s timeline. - * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled. - * @param allowDynamicClippingUpdates Whether the clipping of active media periods moves with a - * live window. If {@code false}, playback ends when it reaches {@code endPositionUs} in the - * last reported live window at the time a media period was created. - * @param relativeToDefaultPosition Whether {@code startPositionUs} and {@code endPositionUs} are - * relative to the default position in the window in {@code mediaSource}'s timeline. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public ClippingMediaSource( MediaSource mediaSource, long startPositionUs, @@ -190,13 +297,22 @@ public final class ClippingMediaSource extends WrappingMediaSource { boolean enableInitialDiscontinuity, boolean allowDynamicClippingUpdates, boolean relativeToDefaultPosition) { - super(Assertions.checkNotNull(mediaSource)); - Assertions.checkArgument(startPositionUs >= 0); - startUs = startPositionUs; - endUs = endPositionUs; - this.enableInitialDiscontinuity = enableInitialDiscontinuity; - this.allowDynamicClippingUpdates = allowDynamicClippingUpdates; - this.relativeToDefaultPosition = relativeToDefaultPosition; + this( + new Builder(mediaSource) + .setStartPositionUs(startPositionUs) + .setEndPositionUs(endPositionUs) + .setEnableInitialDiscontinuity(enableInitialDiscontinuity) + .setAllowDynamicClippingUpdates(allowDynamicClippingUpdates) + .setRelativeToDefaultPosition(relativeToDefaultPosition)); + } + + private ClippingMediaSource(Builder builder) { + super(builder.mediaSource); + this.startUs = builder.startPositionUs; + this.endUs = builder.endPositionUs; + this.enableInitialDiscontinuity = builder.enableInitialDiscontinuity; + this.allowDynamicClippingUpdates = builder.allowDynamicClippingUpdates; + this.relativeToDefaultPosition = builder.relativeToDefaultPosition; mediaPeriods = new ArrayList<>(); window = new Timeline.Window(); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java index b56f6d8a93..f2d4b14542 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java @@ -583,13 +583,13 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { && !mediaItem.clippingConfiguration.relativeToDefaultPosition) { return mediaSource; } - return new ClippingMediaSource( - mediaSource, - mediaItem.clippingConfiguration.startPositionUs, - mediaItem.clippingConfiguration.endPositionUs, - /* enableInitialDiscontinuity= */ !mediaItem.clippingConfiguration.startsAtKeyFrame, - /* allowDynamicClippingUpdates= */ mediaItem.clippingConfiguration.relativeToLiveWindow, - mediaItem.clippingConfiguration.relativeToDefaultPosition); + return new ClippingMediaSource.Builder(mediaSource) + .setStartPositionUs(mediaItem.clippingConfiguration.startPositionUs) + .setEndPositionUs(mediaItem.clippingConfiguration.endPositionUs) + .setEnableInitialDiscontinuity(!mediaItem.clippingConfiguration.startsAtKeyFrame) + .setAllowDynamicClippingUpdates(mediaItem.clippingConfiguration.relativeToLiveWindow) + .setRelativeToDefaultPosition(mediaItem.clippingConfiguration.relativeToDefaultPosition) + .build(); } private MediaSource maybeWrapWithAdsMediaSource(MediaItem mediaItem, MediaSource mediaSource) { diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index 670e6c6d18..acc8d737a3 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -694,16 +694,18 @@ public class ExoPlayerTest { .build(); // Use media sources with discontinuities so that enabled streams are set to final. ClippingMediaSource clippedFakeAudioSource = - new ClippingMediaSource( - new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.AUDIO_FORMAT), 0, 300_000L); + new ClippingMediaSource.Builder( + new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.AUDIO_FORMAT)) + .setEndPositionMs(300) + .build(); ClippingMediaSource clippedFakeAudioVideoSource = - new ClippingMediaSource( - new FakeMediaSource( - new FakeTimeline(), - ExoPlayerTestRunner.VIDEO_FORMAT, - ExoPlayerTestRunner.AUDIO_FORMAT), - 0, - 300_000L); + new ClippingMediaSource.Builder( + new FakeMediaSource( + new FakeTimeline(), + ExoPlayerTestRunner.VIDEO_FORMAT, + ExoPlayerTestRunner.AUDIO_FORMAT)) + .setEndPositionMs(300) + .build(); player.setMediaSources( ImmutableList.of( clippedFakeAudioSource, clippedFakeAudioVideoSource, clippedFakeAudioSource)); @@ -3825,8 +3827,10 @@ public class ExoPlayerTest { long startPositionUs = 300_000; long expectedDurationUs = 700_000; MediaSource mediaSource = - new ClippingMediaSource( - new FakeMediaSource(), startPositionUs, startPositionUs + expectedDurationUs); + new ClippingMediaSource.Builder(new FakeMediaSource()) + .setStartPositionUs(startPositionUs) + .setEndPositionUs(startPositionUs + expectedDurationUs) + .build(); Clock clock = new FakeClock(/* isAutoAdvancing= */ true); AtomicReference playerReference = new AtomicReference<>(); AtomicLong positionAtDiscontinuityMs = new AtomicLong(C.TIME_UNSET); @@ -3964,10 +3968,9 @@ public class ExoPlayerTest { throws Exception { FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null); MediaSource clippedMediaSource = - new ClippingMediaSource( - mediaSource, - /* startPositionUs= */ 3 * C.MICROS_PER_SECOND, - /* endPositionUs= */ C.TIME_END_OF_SOURCE); + new ClippingMediaSource.Builder(mediaSource) + .setStartPositionUs(3 * C.MICROS_PER_SECOND) + .build(); MediaSource concatenatedMediaSource = new ConcatenatingMediaSource(clippedMediaSource); AtomicLong positionWhenReady = new AtomicLong(); ActionSchedule actionSchedule = diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/MergingPlaylistPlaybackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/MergingPlaylistPlaybackTest.java index 454417165d..5c6cab1450 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/MergingPlaylistPlaybackTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/MergingPlaylistPlaybackTest.java @@ -188,13 +188,17 @@ public final class MergingPlaylistPlaybackTest { C.TRACK_TYPE_AUDIO); if (videoClipped) { videoSource = - new ClippingMediaSource( - videoSource, /* startPositionUs= */ 300_000, /* endPositionUs= */ 600_000); + new ClippingMediaSource.Builder(videoSource) + .setStartPositionMs(300) + .setEndPositionMs(600) + .build(); } if (audioClipped) { audioSource = - new ClippingMediaSource( - audioSource, /* startPositionUs= */ 500_000, /* endPositionUs= */ 800_000); + new ClippingMediaSource.Builder(audioSource) + .setStartPositionMs(500) + .setEndPositionMs(800) + .build(); } return new MergingMediaSource( /* adjustPeriodTimeOffsets= */ true, diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ClippingMediaSourceTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ClippingMediaSourceTest.java index deb0a078aa..e2f765b6e3 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ClippingMediaSourceTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ClippingMediaSourceTest.java @@ -15,7 +15,6 @@ */ package androidx.media3.exoplayer.source; -import static androidx.media3.common.util.Util.msToUs; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; @@ -574,13 +573,13 @@ public final class ClippingMediaSourceTest { FakeMediaSource fakeMediaSource = new FakeMediaSource(); fakeMediaSource.setCanUpdateMediaItems(true); fakeMediaSource.updateMediaItem(mediaItem); - return new ClippingMediaSource( - fakeMediaSource, - msToUs(mediaItem.clippingConfiguration.startPositionMs), - msToUs(mediaItem.clippingConfiguration.endPositionMs), - mediaItem.clippingConfiguration.startsAtKeyFrame, - mediaItem.clippingConfiguration.relativeToLiveWindow, - mediaItem.clippingConfiguration.relativeToDefaultPosition); + return new ClippingMediaSource.Builder(fakeMediaSource) + .setStartPositionMs(mediaItem.clippingConfiguration.startPositionMs) + .setEndPositionMs(mediaItem.clippingConfiguration.endPositionMs) + .setEnableInitialDiscontinuity(!mediaItem.clippingConfiguration.startsAtKeyFrame) + .setAllowDynamicClippingUpdates(mediaItem.clippingConfiguration.relativeToLiveWindow) + .setRelativeToDefaultPosition(mediaItem.clippingConfiguration.relativeToDefaultPosition) + .build(); } /** @@ -589,7 +588,11 @@ public final class ClippingMediaSourceTest { private static Timeline getClippedTimeline(Timeline timeline, long startUs, long endUs) throws IOException { FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline); - ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startUs, endUs); + ClippingMediaSource mediaSource = + new ClippingMediaSource.Builder(fakeMediaSource) + .setStartPositionUs(startUs) + .setEndPositionUs(endUs) + .build(); return getClippedTimelines(fakeMediaSource, mediaSource)[0]; } @@ -599,7 +602,11 @@ public final class ClippingMediaSourceTest { private static Timeline getClippedTimeline(Timeline timeline, long durationUs) throws IOException { FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline); - ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, durationUs); + ClippingMediaSource mediaSource = + new ClippingMediaSource.Builder(fakeMediaSource) + .setEndPositionUs(durationUs) + .setRelativeToDefaultPosition(true) + .build(); return getClippedTimelines(fakeMediaSource, mediaSource)[0]; } @@ -617,13 +624,12 @@ public final class ClippingMediaSourceTest { throws IOException { FakeMediaSource fakeMediaSource = new FakeMediaSource(firstTimeline); ClippingMediaSource mediaSource = - new ClippingMediaSource( - fakeMediaSource, - startUs, - endUs, - /* enableInitialDiscontinuity= */ true, - allowDynamicUpdates, - fromDefaultPosition); + new ClippingMediaSource.Builder(fakeMediaSource) + .setStartPositionUs(startUs) + .setEndPositionUs(endUs) + .setAllowDynamicClippingUpdates(allowDynamicUpdates) + .setRelativeToDefaultPosition(fromDefaultPosition) + .build(); return getClippedTimelines(fakeMediaSource, mediaSource, additionalTimelines); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java index 198a924bd7..14ec25115a 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java @@ -814,10 +814,10 @@ public final class CompositionPlayer extends SimpleBasePlayer } MediaSource silenceMediaSource = - new ClippingMediaSource( - new SilenceMediaSource(editedMediaItem.durationUs), - editedMediaItem.mediaItem.clippingConfiguration.startPositionUs, - editedMediaItem.mediaItem.clippingConfiguration.endPositionUs); + new ClippingMediaSource.Builder(new SilenceMediaSource(editedMediaItem.durationUs)) + .setStartPositionUs(editedMediaItem.mediaItem.clippingConfiguration.startPositionUs) + .setEndPositionUs(editedMediaItem.mediaItem.clippingConfiguration.endPositionUs) + .build(); return new MergingMediaSource(mainMediaSource, silenceMediaSource); }