add start and end position to media item

PiperOrigin-RevId: 304795753
This commit is contained in:
bachinger 2020-04-04 16:48:45 +01:00 committed by Oliver Woodman
parent 8583cd92c7
commit fc785871b3
4 changed files with 283 additions and 5 deletions

View File

@ -57,6 +57,11 @@ public final class MediaItem {
@Nullable private String mediaId; @Nullable private String mediaId;
@Nullable private Uri sourceUri; @Nullable private Uri sourceUri;
@Nullable private String mimeType; @Nullable private String mimeType;
private long clipStartPositionMs;
private long clipEndPositionMs;
private boolean clipRelativeToLiveWindow;
private boolean clipRelativeToDefaultPosition;
private boolean clipStartsAtKeyFrame;
@Nullable private Uri drmLicenseUri; @Nullable private Uri drmLicenseUri;
private Map<String, String> drmLicenseRequestHeaders; private Map<String, String> drmLicenseRequestHeaders;
@Nullable private UUID drmUuid; @Nullable private UUID drmUuid;
@ -74,6 +79,7 @@ public final class MediaItem {
subtitles = Collections.emptyList(); subtitles = Collections.emptyList();
drmSessionForClearTypes = Collections.emptyList(); drmSessionForClearTypes = Collections.emptyList();
drmLicenseRequestHeaders = Collections.emptyMap(); drmLicenseRequestHeaders = Collections.emptyMap();
clipEndPositionMs = C.TIME_END_OF_SOURCE;
} }
/** /**
@ -117,6 +123,55 @@ public final class MediaItem {
return this; return this;
} }
/**
* Sets the optional start position in milliseconds which must be a value larger than or equal
* to zero (Default: 0).
*/
public Builder setClipStartPositionMs(long startPositionMs) {
Assertions.checkArgument(startPositionMs >= 0);
this.clipStartPositionMs = startPositionMs;
return this;
}
/**
* 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}).
*/
public Builder setClipEndPositionMs(long endPositionMs) {
Assertions.checkArgument(endPositionMs == C.TIME_END_OF_SOURCE || endPositionMs >= 0);
this.clipEndPositionMs = endPositionMs;
return this;
}
/**
* Sets whether the start/end positions should move with the live window for live streams. If
* {@code false}, live streams end when playback reaches the end position in live window seen
* when the media is first loaded (Default: {@code false}).
*/
public Builder setClipRelativeToLiveWindow(boolean relativeToLiveWindow) {
this.clipRelativeToLiveWindow = relativeToLiveWindow;
return this;
}
/**
* Sets whether the start position and the end position are relative to the default position in
* the window (Default: {@code false}).
*/
public Builder setClipRelativeToDefaultPosition(boolean relativeToDefaultPosition) {
this.clipRelativeToDefaultPosition = relativeToDefaultPosition;
return this;
}
/**
* Sets whether the start point is guaranteed to be a key frame. If {@code false}, the playback
* transition into the clip may not be seamless (Default: {@code false}).
*/
public Builder setClipStartsAtKeyFrame(boolean startsAtKeyFrame) {
this.clipStartsAtKeyFrame = startsAtKeyFrame;
return this;
}
/** /**
* Sets the optional license server {@link Uri}. If a license uri is set, the {@link * Sets the optional license server {@link Uri}. If a license uri is set, the {@link
* DrmConfiguration#uuid} needs to be specified as well. * DrmConfiguration#uuid} needs to be specified as well.
@ -303,6 +358,12 @@ public final class MediaItem {
} }
return new MediaItem( return new MediaItem(
Assertions.checkNotNull(mediaId), Assertions.checkNotNull(mediaId),
new ClippingProperties(
clipStartPositionMs,
clipEndPositionMs,
clipRelativeToLiveWindow,
clipRelativeToDefaultPosition,
clipStartsAtKeyFrame),
playbackProperties, playbackProperties,
mediaMetadata != null ? mediaMetadata : new MediaMetadata.Builder().build()); mediaMetadata != null ? mediaMetadata : new MediaMetadata.Builder().build());
} }
@ -521,6 +582,75 @@ public final class MediaItem {
} }
} }
/** Optionally clips the media item to a custom start and end position. */
public static final class ClippingProperties {
/** The start position in milliseconds. This is a value larger than or equal to zero. */
public final long startPositionMs;
/**
* 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;
/**
* Whether the clipping of active media periods moves with a live window. If {@code false},
* playback ends when it reaches {@link #endPositionMs}.
*/
public final boolean relativeToLiveWindow;
/**
* Whether {@link #startPositionMs} and {@link #endPositionMs} are relative to the default
* position.
*/
public final boolean relativeToDefaultPosition;
/** Sets whether the start point is guaranteed to be a key frame. */
public final boolean startsAtKeyFrame;
private ClippingProperties(
long startPositionMs,
long endPositionMs,
boolean relativeToLiveWindow,
boolean relativeToDefaultPosition,
boolean startsAtKeyFrame) {
this.startPositionMs = startPositionMs;
this.endPositionMs = endPositionMs;
this.relativeToLiveWindow = relativeToLiveWindow;
this.relativeToDefaultPosition = relativeToDefaultPosition;
this.startsAtKeyFrame = startsAtKeyFrame;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ClippingProperties)) {
return false;
}
ClippingProperties other = (ClippingProperties) obj;
return startPositionMs == other.startPositionMs
&& endPositionMs == other.endPositionMs
&& relativeToLiveWindow == other.relativeToLiveWindow
&& relativeToDefaultPosition == other.relativeToDefaultPosition
&& startsAtKeyFrame == other.startsAtKeyFrame;
}
@Override
public int hashCode() {
int result = Long.valueOf(startPositionMs).hashCode();
result = 31 * result + Long.valueOf(endPositionMs).hashCode();
result = 31 * result + (relativeToLiveWindow ? 1 : 0);
result = 31 * result + (relativeToDefaultPosition ? 1 : 0);
result = 31 * result + (startsAtKeyFrame ? 1 : 0);
return result;
}
}
/** Identifies the media item. */ /** Identifies the media item. */
public final String mediaId; public final String mediaId;
@ -530,13 +660,18 @@ public final class MediaItem {
/** The media metadata. */ /** The media metadata. */
public final MediaMetadata mediaMetadata; public final MediaMetadata mediaMetadata;
/** The clipping properties. */
public final ClippingProperties clippingProperties;
private MediaItem( private MediaItem(
String mediaId, String mediaId,
ClippingProperties clippingProperties,
@Nullable PlaybackProperties playbackProperties, @Nullable PlaybackProperties playbackProperties,
MediaMetadata mediaMetadata) { MediaMetadata mediaMetadata) {
this.mediaId = mediaId; this.mediaId = mediaId;
this.playbackProperties = playbackProperties; this.playbackProperties = playbackProperties;
this.mediaMetadata = mediaMetadata; this.mediaMetadata = mediaMetadata;
this.clippingProperties = clippingProperties;
} }
@Override @Override
@ -551,6 +686,7 @@ public final class MediaItem {
MediaItem other = (MediaItem) obj; MediaItem other = (MediaItem) obj;
return Util.areEqual(mediaId, other.mediaId) return Util.areEqual(mediaId, other.mediaId)
&& clippingProperties.equals(other.clippingProperties)
&& Util.areEqual(playbackProperties, other.playbackProperties) && Util.areEqual(playbackProperties, other.playbackProperties)
&& Util.areEqual(mediaMetadata, other.mediaMetadata); && Util.areEqual(mediaMetadata, other.mediaMetadata);
} }
@ -559,6 +695,7 @@ public final class MediaItem {
public int hashCode() { public int hashCode() {
int result = mediaId.hashCode(); int result = mediaId.hashCode();
result = 31 * result + (playbackProperties != null ? playbackProperties.hashCode() : 0); result = 31 * result + (playbackProperties != null ? playbackProperties.hashCode() : 0);
result = 31 * result + clippingProperties.hashCode();
result = 31 * result + mediaMetadata.hashCode(); result = 31 * result + mediaMetadata.hashCode();
return result; return result;
} }

View File

@ -216,11 +216,11 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
MediaSource leafMediaSource = mediaSourceFactory.createMediaSource(mediaItem); MediaSource leafMediaSource = mediaSourceFactory.createMediaSource(mediaItem);
if (mediaItem.playbackProperties.subtitles.isEmpty()) { List<MediaItem.Subtitle> subtitles = mediaItem.playbackProperties.subtitles;
return leafMediaSource; if (subtitles.isEmpty()) {
return maybeClipMediaSource(mediaItem, leafMediaSource);
} }
List<MediaItem.Subtitle> subtitles = mediaItem.playbackProperties.subtitles;
MediaSource[] mediaSources = new MediaSource[subtitles.size() + 1]; MediaSource[] mediaSources = new MediaSource[subtitles.size() + 1];
mediaSources[0] = leafMediaSource; mediaSources[0] = leafMediaSource;
SingleSampleMediaSource.Factory singleSampleSourceFactory = SingleSampleMediaSource.Factory singleSampleSourceFactory =
@ -234,9 +234,10 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
.setSelectionFlags(subtitle.selectionFlags) .setSelectionFlags(subtitle.selectionFlags)
.build(); .build();
mediaSources[i + 1] = mediaSources[i + 1] =
singleSampleSourceFactory.createMediaSource(subtitle.uri, subtitleFormat, C.TIME_UNSET); singleSampleSourceFactory.createMediaSource(
subtitle.uri, subtitleFormat, /* durationUs= */ C.TIME_UNSET);
} }
return new MergingMediaSource(mediaSources); return maybeClipMediaSource(mediaItem, new MergingMediaSource(mediaSources));
} }
// internal methods // internal methods
@ -269,6 +270,21 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
return drmCallback; return drmCallback;
} }
private static MediaSource maybeClipMediaSource(MediaItem mediaItem, MediaSource mediaSource) {
if (mediaItem.clippingProperties.startPositionMs == 0
&& mediaItem.clippingProperties.endPositionMs == C.TIME_END_OF_SOURCE
&& !mediaItem.clippingProperties.relativeToDefaultPosition) {
return mediaSource;
}
return new ClippingMediaSource(
mediaSource,
C.msToUs(mediaItem.clippingProperties.startPositionMs),
C.msToUs(mediaItem.clippingProperties.endPositionMs),
/* enableInitialDiscontinuity= */ !mediaItem.clippingProperties.startsAtKeyFrame,
/* allowDynamicClippingUpdates= */ mediaItem.clippingProperties.relativeToLiveWindow,
mediaItem.clippingProperties.relativeToDefaultPosition);
}
private static SparseArray<MediaSourceFactory> loadDelegates( private static SparseArray<MediaSourceFactory> loadDelegates(
DataSource.Factory dataSourceFactory) { DataSource.Factory dataSourceFactory) {
SparseArray<MediaSourceFactory> factories = new SparseArray<>(); SparseArray<MediaSourceFactory> factories = new SparseArray<>();

View File

@ -187,6 +187,77 @@ public class MediaItemTest {
assertThat(mediaItem.playbackProperties.tag).isEqualTo(tag); assertThat(mediaItem.playbackProperties.tag).isEqualTo(tag);
} }
@Test
public void builderSetStartPositionMs_setsStartPositionMs() {
MediaItem mediaItem =
new MediaItem.Builder().setSourceUri(URI_STRING).setClipStartPositionMs(1000L).build();
assertThat(mediaItem.clippingProperties.startPositionMs).isEqualTo(1000L);
}
@Test
public void builderSetStartPositionMs_zeroByDefault() {
MediaItem mediaItem = new MediaItem.Builder().setSourceUri(URI_STRING).build();
assertThat(mediaItem.clippingProperties.startPositionMs).isEqualTo(0);
}
@Test
public void builderSetStartPositionMs_negativeValue_throws() {
MediaItem.Builder builder = new MediaItem.Builder();
assertThrows(IllegalArgumentException.class, () -> builder.setClipStartPositionMs(-1));
}
@Test
public void builderSetEndPositionMs_setsEndPositionMs() {
MediaItem mediaItem =
new MediaItem.Builder().setSourceUri(URI_STRING).setClipEndPositionMs(1000L).build();
assertThat(mediaItem.clippingProperties.endPositionMs).isEqualTo(1000L);
}
@Test
public void builderSetEndPositionMs_timeEndOfSourceByDefault() {
MediaItem mediaItem = new MediaItem.Builder().setSourceUri(URI_STRING).build();
assertThat(mediaItem.clippingProperties.endPositionMs).isEqualTo(C.TIME_END_OF_SOURCE);
}
@Test
public void builderSetEndPositionMs_timeEndOfSource_setsEndPositionMs() {
MediaItem mediaItem =
new MediaItem.Builder()
.setSourceUri(URI_STRING)
.setClipEndPositionMs(1000)
.setClipEndPositionMs(C.TIME_END_OF_SOURCE)
.build();
assertThat(mediaItem.clippingProperties.endPositionMs).isEqualTo(C.TIME_END_OF_SOURCE);
}
@Test
public void builderSetEndPositionMs_negativeValue_throws() {
MediaItem.Builder builder = new MediaItem.Builder();
assertThrows(IllegalArgumentException.class, () -> builder.setClipEndPositionMs(-1));
}
@Test
public void builderSetClippingFlags_setsClippingFlags() {
MediaItem mediaItem =
new MediaItem.Builder()
.setSourceUri(URI_STRING)
.setClipRelativeToDefaultPosition(true)
.setClipRelativeToLiveWindow(true)
.setClipStartsAtKeyFrame(true)
.build();
assertThat(mediaItem.clippingProperties.relativeToDefaultPosition).isTrue();
assertThat(mediaItem.clippingProperties.relativeToLiveWindow).isTrue();
assertThat(mediaItem.clippingProperties.startsAtKeyFrame).isTrue();
}
@Test @Test
public void builderSetMediaMetadata_setsMetadata() { public void builderSetMediaMetadata_setsMetadata() {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build();

View File

@ -122,6 +122,60 @@ public final class DefaultMediaSourceFactoryTest {
assertThat(mediaSource.getTag()).isEqualTo(tag); assertThat(mediaSource.getTag()).isEqualTo(tag);
} }
@Test
public void createMediaSource_withStartPosition_isClippingMediaSource() {
DefaultMediaSourceFactory defaultMediaSourceFactory =
DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext());
MediaItem mediaItem =
new MediaItem.Builder().setSourceUri(URI_MEDIA).setClipStartPositionMs(1000L).build();
MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem);
assertThat(mediaSource).isInstanceOf(ClippingMediaSource.class);
}
@Test
public void createMediaSource_withEndPosition_isClippingMediaSource() {
DefaultMediaSourceFactory defaultMediaSourceFactory =
DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext());
MediaItem mediaItem =
new MediaItem.Builder().setSourceUri(URI_MEDIA).setClipEndPositionMs(1000L).build();
MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem);
assertThat(mediaSource).isInstanceOf(ClippingMediaSource.class);
}
@Test
public void createMediaSource_relativeToDefaultPosition_isClippingMediaSource() {
DefaultMediaSourceFactory defaultMediaSourceFactory =
DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext());
MediaItem mediaItem =
new MediaItem.Builder()
.setSourceUri(URI_MEDIA)
.setClipRelativeToDefaultPosition(true)
.build();
MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem);
assertThat(mediaSource).isInstanceOf(ClippingMediaSource.class);
}
@Test
public void createMediaSource_defaultToEnd_isNotClippingMediaSource() {
DefaultMediaSourceFactory defaultMediaSourceFactory =
DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext());
MediaItem mediaItem =
new MediaItem.Builder()
.setSourceUri(URI_MEDIA)
.setClipEndPositionMs(C.TIME_END_OF_SOURCE)
.build();
MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem);
assertThat(mediaSource).isInstanceOf(ProgressiveMediaSource.class);
}
@Test @Test
public void getSupportedTypes_coreModule_onlyOther() { public void getSupportedTypes_coreModule_onlyOther() {
int[] supportedTypes = int[] supportedTypes =