diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b6f7083e3b..47d304387a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,8 @@ handle these additional calls with `mediaItemIndex == C.INDEX_UNSET`. * Remove compile dependency on enhanced Java 8 desugaring ([#1312](https://github.com/androidx/media/issues/1312)). + * Ensure the duration passed to `MediaItem.Builder.setImageDurationMs` is + ignored for a non-image `MediaItem` (as documented). * ExoPlayer: * Add `reset` to `BasePreloadManager` to release all the holding sources while keep the preload manager instance. 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 3d7ae4cc98..55416f4f90 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaItem.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaItem.java @@ -573,7 +573,10 @@ public final class MediaItem implements Bundleable { /** * Sets the image duration in video output, in milliseconds. * - *

Must be set if {@linkplain #setUri the uri} is set and resolves to an image. Ignored + *

Must be set if {@linkplain #setUri the URI} is set and resolves to an image. Ignored + * otherwise. + * + *

Motion photos will be rendered as images if this parameter is set, and as videos * otherwise. * *

Default value is {@link C#TIME_UNSET}. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java index b1b7ab607d..6e2672525d 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java @@ -114,13 +114,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final Allocator allocator; @Nullable private final String customCacheKey; private final long continueLoadingCheckIntervalBytes; + private final long singleSampleDurationUs; private final Loader loader; private final ProgressiveMediaExtractor progressiveMediaExtractor; private final ConditionVariable loadCondition; private final Runnable maybeFinishPrepareRunnable; private final Runnable onContinueLoadingRequestedRunnable; private final Handler handler; - private final boolean isSingleSample; @Nullable private Callback callback; @Nullable private IcyHeaders icyHeaders; @@ -130,6 +130,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private boolean prepared; private boolean haveAudioVideoTracks; + private boolean isSingleSample; private @MonotonicNonNull TrackState trackState; private @MonotonicNonNull SeekMap seekMap; private long durationUs; @@ -194,8 +195,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; loader = new Loader("ProgressiveMediaPeriod"); this.progressiveMediaExtractor = progressiveMediaExtractor; - this.durationUs = singleSampleDurationUs; - isSingleSample = singleSampleDurationUs != C.TIME_UNSET; + this.singleSampleDurationUs = singleSampleDurationUs; loadCondition = new ConditionVariable(); maybeFinishPrepareRunnable = this::maybeFinishPrepare; onContinueLoadingRequestedRunnable = @@ -745,20 +745,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private void setSeekMap(SeekMap seekMap) { this.seekMap = icyHeaders == null ? seekMap : new Unseekable(/* durationUs= */ C.TIME_UNSET); - if (seekMap.getDurationUs() == C.TIME_UNSET && durationUs != C.TIME_UNSET) { - this.seekMap = - new ForwardingSeekMap(this.seekMap) { - @Override - public long getDurationUs() { - return durationUs; - } - }; - } - durationUs = this.seekMap.getDurationUs(); + durationUs = seekMap.getDurationUs(); isLive = !isLengthKnown && seekMap.getDurationUs() == C.TIME_UNSET; dataType = isLive ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE : C.DATA_TYPE_MEDIA; - listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive); - if (!prepared) { + if (prepared) { + listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive); + } else { maybeFinishPrepare(); } } @@ -783,6 +775,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; boolean isAudioVideo = isAudio || MimeTypes.isVideo(mimeType); trackIsAudioVideoFlags[i] = isAudioVideo; haveAudioVideoTracks |= isAudioVideo; + boolean isImage = MimeTypes.isImage(mimeType); + isSingleSample = singleSampleDurationUs != C.TIME_UNSET && trackCount == 1 && isImage; @Nullable IcyHeaders icyHeaders = this.icyHeaders; if (icyHeaders != null) { if (isAudio || sampleQueueTrackIds[i].isIcyTrack) { @@ -807,6 +801,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; trackArray[i] = new TrackGroup(/* id= */ Integer.toString(i), trackFormat); } trackState = new TrackState(new TrackGroupArray(trackArray), trackIsAudioVideoFlags); + if (isSingleSample && durationUs == C.TIME_UNSET) { + durationUs = singleSampleDurationUs; + seekMap = + new ForwardingSeekMap(seekMap) { + @Override + public long getDurationUs() { + return durationUs; + } + }; + } + listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive); prepared = true; checkNotNull(callback).onPrepared(this); }