diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaItem.java b/libraries/common/src/main/java/androidx/media3/common/MediaItem.java index 9880bcec64..f6eb006197 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaItem.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaItem.java @@ -18,6 +18,8 @@ package androidx.media3.common; 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 androidx.media3.common.util.Util.usToMs; import android.net.Uri; import android.os.Bundle; @@ -1837,20 +1839,20 @@ public final class MediaItem implements Bundleable { /** Builder for {@link ClippingConfiguration} instances. */ public static final class Builder { - private long startPositionMs; - private long endPositionMs; + private long startPositionUs; + private long endPositionUs; private boolean relativeToLiveWindow; private boolean relativeToDefaultPosition; private boolean startsAtKeyFrame; /** Creates a new instance with default values. */ public Builder() { - endPositionMs = C.TIME_END_OF_SOURCE; + endPositionUs = C.TIME_END_OF_SOURCE; } private Builder(ClippingConfiguration clippingConfiguration) { - startPositionMs = clippingConfiguration.startPositionMs; - endPositionMs = clippingConfiguration.endPositionMs; + startPositionUs = clippingConfiguration.startPositionUs; + endPositionUs = clippingConfiguration.endPositionUs; relativeToLiveWindow = clippingConfiguration.relativeToLiveWindow; relativeToDefaultPosition = clippingConfiguration.relativeToDefaultPosition; startsAtKeyFrame = clippingConfiguration.startsAtKeyFrame; @@ -1862,8 +1864,18 @@ public final class MediaItem implements Bundleable { */ @CanIgnoreReturnValue public Builder setStartPositionMs(@IntRange(from = 0) long startPositionMs) { - Assertions.checkArgument(startPositionMs >= 0); - this.startPositionMs = startPositionMs; + return setStartPositionUs(msToUs(startPositionMs)); + } + + /** + * Sets the optional start position in microseconds which must be a value larger than or equal + * to zero (Default: 0). + */ + @UnstableApi + @CanIgnoreReturnValue + public Builder setStartPositionUs(@IntRange(from = 0) long startPositionUs) { + Assertions.checkArgument(startPositionUs >= 0); + this.startPositionUs = startPositionUs; return this; } @@ -1874,8 +1886,19 @@ public final class MediaItem implements Bundleable { */ @CanIgnoreReturnValue public Builder setEndPositionMs(long endPositionMs) { - Assertions.checkArgument(endPositionMs == C.TIME_END_OF_SOURCE || endPositionMs >= 0); - this.endPositionMs = endPositionMs; + return setEndPositionUs(msToUs(endPositionMs)); + } + + /** + * Sets the optional end position in milliseconds which must be a value larger than or equal + * to zero, or {@link C#TIME_END_OF_SOURCE} to end when playback reaches the end of media + * (Default: {@link C#TIME_END_OF_SOURCE}). + */ + @UnstableApi + @CanIgnoreReturnValue + public Builder setEndPositionUs(long endPositionUs) { + Assertions.checkArgument(endPositionUs == C.TIME_END_OF_SOURCE || endPositionUs >= 0); + this.endPositionUs = endPositionUs; return this; } @@ -1932,12 +1955,23 @@ public final class MediaItem implements Bundleable { @IntRange(from = 0) public final long startPositionMs; + /** The start position in microseconds. This is a value larger than or equal to zero. */ + @UnstableApi + @IntRange(from = 0) + public final long startPositionUs; + /** * The end position in milliseconds. This is a value larger than or equal to zero or {@link * C#TIME_END_OF_SOURCE} to play to the end of the stream. */ public final long endPositionMs; + /** + * The end position in microseconds. This is a value larger than or equal to zero or {@link + * C#TIME_END_OF_SOURCE} to play to the end of the stream. + */ + @UnstableApi public final long endPositionUs; + /** * Whether the clipping of active media periods moves with a live window. If {@code false}, * playback ends when it reaches {@link #endPositionMs}. @@ -1954,8 +1988,10 @@ public final class MediaItem implements Bundleable { public final boolean startsAtKeyFrame; private ClippingConfiguration(Builder builder) { - this.startPositionMs = builder.startPositionMs; - this.endPositionMs = builder.endPositionMs; + this.startPositionMs = usToMs(builder.startPositionUs); + this.endPositionMs = usToMs(builder.endPositionUs); + this.startPositionUs = builder.startPositionUs; + this.endPositionUs = builder.endPositionUs; this.relativeToLiveWindow = builder.relativeToLiveWindow; this.relativeToDefaultPosition = builder.relativeToDefaultPosition; this.startsAtKeyFrame = builder.startsAtKeyFrame; @@ -1977,8 +2013,8 @@ public final class MediaItem implements Bundleable { ClippingConfiguration other = (ClippingConfiguration) obj; - return startPositionMs == other.startPositionMs - && endPositionMs == other.endPositionMs + return startPositionUs == other.startPositionUs + && endPositionUs == other.endPositionUs && relativeToLiveWindow == other.relativeToLiveWindow && relativeToDefaultPosition == other.relativeToDefaultPosition && startsAtKeyFrame == other.startsAtKeyFrame; @@ -1986,8 +2022,8 @@ public final class MediaItem implements Bundleable { @Override public int hashCode() { - int result = (int) (startPositionMs ^ (startPositionMs >>> 32)); - result = 31 * result + (int) (endPositionMs ^ (endPositionMs >>> 32)); + int result = (int) (startPositionUs ^ (startPositionUs >>> 32)); + result = 31 * result + (int) (endPositionUs ^ (endPositionUs >>> 32)); result = 31 * result + (relativeToLiveWindow ? 1 : 0); result = 31 * result + (relativeToDefaultPosition ? 1 : 0); result = 31 * result + (startsAtKeyFrame ? 1 : 0); @@ -2001,6 +2037,8 @@ public final class MediaItem implements Bundleable { private static final String FIELD_RELATIVE_TO_LIVE_WINDOW = Util.intToStringMaxRadix(2); private static final String FIELD_RELATIVE_TO_DEFAULT_POSITION = Util.intToStringMaxRadix(3); private static final String FIELD_STARTS_AT_KEY_FRAME = Util.intToStringMaxRadix(4); + static final String FIELD_START_POSITION_US = Util.intToStringMaxRadix(5); + static final String FIELD_END_POSITION_US = Util.intToStringMaxRadix(6); @UnstableApi @Override @@ -2012,6 +2050,12 @@ public final class MediaItem implements Bundleable { if (endPositionMs != UNSET.endPositionMs) { bundle.putLong(FIELD_END_POSITION_MS, endPositionMs); } + if (startPositionUs != UNSET.startPositionUs) { + bundle.putLong(FIELD_START_POSITION_US, startPositionUs); + } + if (endPositionUs != UNSET.endPositionUs) { + bundle.putLong(FIELD_END_POSITION_US, endPositionUs); + } if (relativeToLiveWindow != UNSET.relativeToLiveWindow) { bundle.putBoolean(FIELD_RELATIVE_TO_LIVE_WINDOW, relativeToLiveWindow); } @@ -2027,25 +2071,38 @@ public final class MediaItem implements Bundleable { /** An object that can restore {@link ClippingConfiguration} from a {@link Bundle}. */ @UnstableApi public static final Creator CREATOR = - bundle -> - new ClippingConfiguration.Builder() - .setStartPositionMs( - bundle.getLong( - FIELD_START_POSITION_MS, /* defaultValue= */ UNSET.startPositionMs)) - .setEndPositionMs( - bundle.getLong(FIELD_END_POSITION_MS, /* defaultValue= */ UNSET.endPositionMs)) - .setRelativeToLiveWindow( - bundle.getBoolean( - FIELD_RELATIVE_TO_LIVE_WINDOW, - /* defaultValue= */ UNSET.relativeToLiveWindow)) - .setRelativeToDefaultPosition( - bundle.getBoolean( - FIELD_RELATIVE_TO_DEFAULT_POSITION, - /* defaultValue= */ UNSET.relativeToDefaultPosition)) - .setStartsAtKeyFrame( - bundle.getBoolean( - FIELD_STARTS_AT_KEY_FRAME, /* defaultValue= */ UNSET.startsAtKeyFrame)) - .buildClippingProperties(); + bundle -> { + ClippingConfiguration.Builder clippingConfiguration = + new ClippingConfiguration.Builder() + .setStartPositionMs( + bundle.getLong( + FIELD_START_POSITION_MS, /* defaultValue= */ UNSET.startPositionMs)) + .setEndPositionMs( + bundle.getLong( + FIELD_END_POSITION_MS, /* defaultValue= */ UNSET.endPositionMs)) + .setRelativeToLiveWindow( + bundle.getBoolean( + FIELD_RELATIVE_TO_LIVE_WINDOW, + /* defaultValue= */ UNSET.relativeToLiveWindow)) + .setRelativeToDefaultPosition( + bundle.getBoolean( + FIELD_RELATIVE_TO_DEFAULT_POSITION, + /* defaultValue= */ UNSET.relativeToDefaultPosition)) + .setStartsAtKeyFrame( + bundle.getBoolean( + FIELD_STARTS_AT_KEY_FRAME, /* defaultValue= */ UNSET.startsAtKeyFrame)); + long startPositionUs = + bundle.getLong(FIELD_START_POSITION_US, /* defaultValue= */ UNSET.startPositionUs); + if (startPositionUs != UNSET.startPositionUs) { + clippingConfiguration.setStartPositionUs(startPositionUs); + } + long endPositionUs = + bundle.getLong(FIELD_END_POSITION_US, /* defaultValue= */ UNSET.endPositionUs); + if (endPositionUs != UNSET.endPositionUs) { + clippingConfiguration.setEndPositionUs(endPositionUs); + } + return clippingConfiguration.buildClippingProperties(); + }; } /** diff --git a/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java b/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java index d7e38a0436..125f3c981c 100644 --- a/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java @@ -15,6 +15,8 @@ */ package androidx.media3.common; +import static androidx.media3.common.MediaItem.ClippingConfiguration.FIELD_END_POSITION_US; +import static androidx.media3.common.MediaItem.ClippingConfiguration.FIELD_START_POSITION_US; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; @@ -438,7 +440,9 @@ public class MediaItemTest { // Please refrain from altering default values since doing so would cause issues with backwards // compatibility. assertThat(clippingConfiguration.startPositionMs).isEqualTo(0L); + assertThat(clippingConfiguration.startPositionUs).isEqualTo(0L); assertThat(clippingConfiguration.endPositionMs).isEqualTo(C.TIME_END_OF_SOURCE); + assertThat(clippingConfiguration.endPositionUs).isEqualTo(C.TIME_END_OF_SOURCE); assertThat(clippingConfiguration.relativeToLiveWindow).isFalse(); assertThat(clippingConfiguration.relativeToDefaultPosition).isFalse(); assertThat(clippingConfiguration.startsAtKeyFrame).isFalse(); @@ -468,6 +472,7 @@ public class MediaItemTest { MediaItem.ClippingConfiguration clippingConfiguration = new MediaItem.ClippingConfiguration.Builder() .setStartPositionMs(1000L) + .setEndPositionUs(2000_031L) .setStartsAtKeyFrame(true) .build(); @@ -477,6 +482,51 @@ public class MediaItemTest { assertThat(clippingConfigurationFromBundle).isEqualTo(clippingConfiguration); } + @Test + public void createClippingConfigurationInstance_viaBundleWithOnlyMs_yieldsEqualInstance() { + // Creates instance by setting some non-default values + MediaItem.ClippingConfiguration clippingConfiguration = + new MediaItem.ClippingConfiguration.Builder() + .setStartPositionMs(1000L) + .setEndPositionMs(2000L) + .setStartsAtKeyFrame(true) + .build(); + Bundle clippingConfigurationBundle = clippingConfiguration.toBundle(); + clippingConfigurationBundle.remove(FIELD_START_POSITION_US); + clippingConfigurationBundle.remove(FIELD_END_POSITION_US); + + MediaItem.ClippingConfiguration clippingConfigurationFromBundle = + MediaItem.ClippingConfiguration.CREATOR.fromBundle(clippingConfigurationBundle); + + assertThat(clippingConfigurationFromBundle).isEqualTo(clippingConfiguration); + } + + @Test + public void createClippingConfigurationInstance_setsStartPositionInMsAndUs_fieldsAreConsistent() { + // Creates instance by setting some non-default values + MediaItem.ClippingConfiguration clippingConfiguration = + new MediaItem.ClippingConfiguration.Builder() + .setStartPositionMs(1000L) + .setStartPositionUs(200_203L) + .build(); + + assertThat(clippingConfiguration.startPositionMs).isEqualTo(200L); + assertThat(clippingConfiguration.startPositionUs).isEqualTo(200_203L); + } + + @Test + public void createClippingConfigurationInstance_setsEndPositionInMsAndUs_fieldsAreConsistent() { + // Creates instance by setting some non-default values + MediaItem.ClippingConfiguration clippingConfiguration = + new MediaItem.ClippingConfiguration.Builder() + .setEndPositionUs(1000L) + .setEndPositionMs(C.TIME_END_OF_SOURCE) + .build(); + + assertThat(clippingConfiguration.endPositionMs).isEqualTo(C.TIME_END_OF_SOURCE); + assertThat(clippingConfiguration.endPositionMs).isEqualTo(C.TIME_END_OF_SOURCE); + } + @Test public void clippingConfigurationBuilder_throwsOnInvalidValues() { MediaItem.ClippingConfiguration.Builder clippingConfigurationBuilder = diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource2.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource2.java index d09b204741..392c80df78 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource2.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource2.java @@ -19,6 +19,7 @@ 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.Assertions.checkStateNotNull; +import static androidx.media3.common.util.Util.usToMs; import android.content.Context; import android.net.Uri; @@ -154,16 +155,17 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource