Add microsecond precision to MediaItem.ClippingConfiguration

PiperOrigin-RevId: 578881990
This commit is contained in:
tofunmi 2023-11-02 09:35:06 -07:00 committed by Copybara-Service
parent dab9eb33a4
commit 3253f1b5cd
6 changed files with 176 additions and 49 deletions

View File

@ -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<ClippingProperties> 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();
};
}
/**

View File

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

View File

@ -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<Intege
@CanIgnoreReturnValue
public Builder add(MediaItem mediaItem, long initialPlaceholderDurationMs) {
checkNotNull(mediaItem);
checkStateNotNull(
mediaSourceFactory,
"Must use useDefaultMediaSourceFactory or setMediaSourceFactory first.");
if (initialPlaceholderDurationMs == C.TIME_UNSET
&& mediaItem.clippingConfiguration.endPositionMs != C.TIME_END_OF_SOURCE) {
// If the item is going to be clipped, we can provide a placeholder duration automatically.
initialPlaceholderDurationMs =
mediaItem.clippingConfiguration.endPositionMs
- mediaItem.clippingConfiguration.startPositionMs;
usToMs(
mediaItem.clippingConfiguration.endPositionUs
- mediaItem.clippingConfiguration.startPositionUs);
}
checkStateNotNull(
mediaSourceFactory,
"Must use useDefaultMediaSourceFactory or setMediaSourceFactory first.");
return add(mediaSourceFactory.createMediaSource(mediaItem), initialPlaceholderDurationMs);
}
@ -364,7 +366,7 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
if (timeOffsetUs == null) {
return mediaTimeMs;
}
return mediaTimeMs + Util.usToMs(timeOffsetUs);
return mediaTimeMs + usToMs(timeOffsetUs);
}
private boolean handleMessage(Message msg) {

View File

@ -554,15 +554,15 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
// internal methods
private static MediaSource maybeClipMediaSource(MediaItem mediaItem, MediaSource mediaSource) {
if (mediaItem.clippingConfiguration.startPositionMs == 0
&& mediaItem.clippingConfiguration.endPositionMs == C.TIME_END_OF_SOURCE
if (mediaItem.clippingConfiguration.startPositionUs == 0
&& mediaItem.clippingConfiguration.endPositionUs == C.TIME_END_OF_SOURCE
&& !mediaItem.clippingConfiguration.relativeToDefaultPosition) {
return mediaSource;
}
return new ClippingMediaSource(
mediaSource,
msToUs(mediaItem.clippingConfiguration.startPositionMs),
msToUs(mediaItem.clippingConfiguration.endPositionMs),
mediaItem.clippingConfiguration.startPositionUs,
mediaItem.clippingConfiguration.endPositionUs,
/* enableInitialDiscontinuity= */ !mediaItem.clippingConfiguration.startsAtKeyFrame,
/* allowDynamicClippingUpdates= */ mediaItem.clippingConfiguration.relativeToLiveWindow,
mediaItem.clippingConfiguration.relativeToDefaultPosition);

View File

@ -17,7 +17,6 @@ package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.msToUs;
import androidx.annotation.IntRange;
import androidx.media3.common.C;
@ -276,12 +275,12 @@ public final class EditedMediaItem {
} else {
MediaItem.ClippingConfiguration clippingConfiguration = mediaItem.clippingConfiguration;
checkArgument(!clippingConfiguration.relativeToDefaultPosition);
if (clippingConfiguration.endPositionMs == C.TIME_END_OF_SOURCE) {
presentationDurationUs = durationUs - msToUs(clippingConfiguration.startPositionMs);
if (clippingConfiguration.endPositionUs == C.TIME_END_OF_SOURCE) {
presentationDurationUs = durationUs - clippingConfiguration.startPositionUs;
} else {
checkArgument(clippingConfiguration.endPositionMs <= durationUs);
checkArgument(clippingConfiguration.endPositionUs <= durationUs);
presentationDurationUs =
msToUs(clippingConfiguration.endPositionMs - clippingConfiguration.startPositionMs);
clippingConfiguration.endPositionUs - clippingConfiguration.startPositionUs;
}
}
}

View File

@ -102,6 +102,25 @@ public final class EditedMediaItemBuilderTest {
assertThat(editedMediaItem.presentationDurationUs).isEqualTo(200_000);
}
@Test
public void duration_withClippingConfigurationAndStartEndPositionInUs() {
MediaItem.ClippingConfiguration clippingConfiguration =
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionUs(300_000)
.setEndPositionUs(500_000)
.build();
MediaItem mediaItem =
new MediaItem.Builder()
.setUri("Uri")
.setClippingConfiguration(clippingConfiguration)
.build();
EditedMediaItem editedMediaItem =
new EditedMediaItem.Builder(mediaItem).setDurationUs(1_000_000).build();
assertThat(editedMediaItem.presentationDurationUs).isEqualTo(200_000);
}
@Test
public void duration_withClippingConfigurationAndStartPosition() {
MediaItem.ClippingConfiguration clippingConfiguration =