Add ClippingMediaSource.Builder

This prevents complicated constructor changes when we add new options.

PiperOrigin-RevId: 721415339
This commit is contained in:
tonihei 2025-01-30 09:20:03 -08:00 committed by Copybara-Service
parent 344214d711
commit df575a8d19
6 changed files with 245 additions and 116 deletions

View File

@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>The end position is relative to the wrapped source's {@link Timeline.Window}, unless
* {@link #setRelativeToDefaultPosition} is set to {@code true}.
*
* <p>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.
*
* <p>The end position is relative to the wrapped source's {@link Timeline.Window}, unless
* {@link #setRelativeToDefaultPosition} is set to {@code true}.
*
* <p>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.
*
* <p>This discontinuity is needed to handle pre-rolling samples from a previous keyframe if the
* start position doesn't fall onto a keyframe.
*
* <p>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.
*
* <p>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.
*
* <p>If {@code false}, playback ends when it reaches {@code endPositionUs} in the last reported
* live window at the time a media period was created.
*
* <p>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}.
*
* <p>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.
*
* <p>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.
*
* <p>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();
}

View File

@ -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) {

View File

@ -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<Player> 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 =

View File

@ -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,

View File

@ -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);
}

View File

@ -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);
}