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