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 55e183477e..172c8e6007 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 @@ -55,6 +55,7 @@ import androidx.media3.exoplayer.upstream.Loader.LoadErrorAction; import androidx.media3.exoplayer.upstream.Loader.Loadable; import androidx.media3.extractor.Extractor; import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.extractor.ForwardingSeekMap; import androidx.media3.extractor.PositionHolder; import androidx.media3.extractor.SeekMap; import androidx.media3.extractor.SeekMap.SeekPoints; @@ -119,6 +120,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final Runnable maybeFinishPrepareRunnable; private final Runnable onContinueLoadingRequestedRunnable; private final Handler handler; + private final boolean isSingleSample; @Nullable private Callback callback; @Nullable private IcyHeaders icyHeaders; @@ -163,6 +165,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * indexing. May be null. * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each * invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. + * @param singleSampleDurationUs The duration of media with a single sample in microseconds. */ // maybeFinishPrepare is not posted to the handler until initialization completes. @SuppressWarnings({"nullness:argument", "nullness:methodref.receiver.bound"}) @@ -177,7 +180,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Listener listener, Allocator allocator, @Nullable String customCacheKey, - int continueLoadingCheckIntervalBytes) { + int continueLoadingCheckIntervalBytes, + long singleSampleDurationUs) { this.uri = uri; this.dataSource = dataSource; this.drmSessionManager = drmSessionManager; @@ -190,6 +194,8 @@ 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; loadCondition = new ConditionVariable(); maybeFinishPrepareRunnable = this::maybeFinishPrepare; onContinueLoadingRequestedRunnable = @@ -202,7 +208,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; sampleQueueTrackIds = new TrackId[0]; sampleQueues = new SampleQueue[0]; pendingResetPositionUs = C.TIME_UNSET; - durationUs = C.TIME_UNSET; dataType = C.DATA_TYPE_MEDIA; } @@ -272,8 +277,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } // We'll always need to seek if this is a first selection to a non-zero position, or if we're - // making a selection having previously disabled all tracks. - boolean seekRequired = seenFirstTrackSelection ? oldEnabledTrackCount == 0 : positionUs != 0; + // making a selection having previously disabled all tracks, except for when we have a single + // sample. + boolean seekRequired = + !isSingleSample && (seenFirstTrackSelection ? oldEnabledTrackCount == 0 : positionUs != 0); // Select new tracks. for (int i = 0; i < selections.length; i++) { if (streams[i] == null && selections[i] != null) { @@ -327,6 +334,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void discardBuffer(long positionUs, boolean toKeyframe) { + if (isSingleSample) { + // Optimize by not discarding buffers. + return; + } assertPrepared(); if (isPendingReset()) { return; @@ -734,7 +745,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private void setSeekMap(SeekMap seekMap) { this.seekMap = icyHeaders == null ? seekMap : new Unseekable(/* durationUs= */ C.TIME_UNSET); - durationUs = seekMap.getDurationUs(); + 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(); 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); @@ -880,7 +900,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; int trackCount = sampleQueues.length; for (int i = 0; i < trackCount; i++) { SampleQueue sampleQueue = sampleQueues[i]; - boolean seekInsideQueue = sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ false); + boolean seekInsideQueue = + isSingleSample + ? sampleQueue.seekTo(sampleQueue.getFirstIndex()) + : sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ false); // If we have AV tracks then an in-buffer seek is successful if the seek into every AV queue // is successful. We ignore whether seeks within non-AV queues are successful in this case, as // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java index 5b54fe5949..343fb6222e 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java @@ -233,7 +233,6 @@ public final class ProgressiveMediaSource extends BaseMediaSource private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy; private final int continueLoadingCheckIntervalBytes; - private boolean timelineIsPlaceholder; private long timelineDurationUs; private boolean timelineIsSeekable; @@ -271,6 +270,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource @Nullable MediaItem.LocalConfiguration newConfiguration = mediaItem.localConfiguration; return newConfiguration != null && newConfiguration.uri.equals(existingConfiguration.uri) + && newConfiguration.imageDurationMs == existingConfiguration.imageDurationMs && Util.areEqual(newConfiguration.customCacheKey, existingConfiguration.customCacheKey); } @@ -311,7 +311,8 @@ public final class ProgressiveMediaSource extends BaseMediaSource this, allocator, localConfiguration.customCacheKey, - continueLoadingCheckIntervalBytes); + continueLoadingCheckIntervalBytes, + Util.msToUs(localConfiguration.imageDurationMs)); } @Override diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriodTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriodTest.java index 77789ed863..942e086567 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriodTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriodTest.java @@ -28,7 +28,10 @@ import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.upstream.DefaultAllocator; import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy; +import androidx.media3.extractor.Extractor; +import androidx.media3.extractor.ExtractorsFactory; import androidx.media3.extractor.mp4.Mp4Extractor; +import androidx.media3.extractor.png.PngExtractor; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.util.concurrent.TimeoutException; @@ -44,18 +47,27 @@ public final class ProgressiveMediaPeriodTest { public void prepareUsingBundledExtractors_updatesSourceInfoBeforeOnPreparedCallback() throws TimeoutException { testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback( - new BundledExtractorsAdapter(Mp4Extractor.FACTORY)); + new BundledExtractorsAdapter(Mp4Extractor.FACTORY), C.TIME_UNSET); + } + + @Test + public void + prepareUsingBundledExtractor_withImageExtractor_updatesSourceInfoBeforeOnPreparedCallback() + throws TimeoutException { + ExtractorsFactory pngExtractorFactory = () -> new Extractor[] {new PngExtractor()}; + testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback( + new BundledExtractorsAdapter(pngExtractorFactory), 5 * C.MICROS_PER_SECOND); } @Test public void prepareUsingMediaParser_updatesSourceInfoBeforeOnPreparedCallback() throws TimeoutException { testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback( - new MediaParserExtractorAdapter(PlayerId.UNSET)); + new MediaParserExtractorAdapter(PlayerId.UNSET), C.TIME_UNSET); } private static void testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback( - ProgressiveMediaExtractor extractor) throws TimeoutException { + ProgressiveMediaExtractor extractor, long imageDurationUs) throws TimeoutException { AtomicBoolean sourceInfoRefreshCalled = new AtomicBoolean(false); ProgressiveMediaPeriod.Listener sourceInfoRefreshListener = (durationUs, isSeekable, isLive) -> sourceInfoRefreshCalled.set(true); @@ -74,7 +86,8 @@ public final class ProgressiveMediaPeriodTest { sourceInfoRefreshListener, new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE), /* customCacheKey= */ null, - ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES); + ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES, + imageDurationUs); AtomicBoolean prepareCallbackCalled = new AtomicBoolean(false); AtomicBoolean sourceInfoRefreshCalledBeforeOnPrepared = new AtomicBoolean(false); diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ForwardingSeekMap.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ForwardingSeekMap.java new file mode 100644 index 0000000000..a116acc0aa --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ForwardingSeekMap.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.extractor; + +import androidx.media3.common.util.UnstableApi; + +/** A forwarding class for {@link SeekMap} */ +@UnstableApi +public class ForwardingSeekMap implements SeekMap { + private final SeekMap seekMap; + + /** + * Creates a instance. + * + * @param seekMap The original {@link SeekMap}. + */ + public ForwardingSeekMap(SeekMap seekMap) { + this.seekMap = seekMap; + } + + @Override + public boolean isSeekable() { + return seekMap.isSeekable(); + } + + @Override + public long getDurationUs() { + return seekMap.getDurationUs(); + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + return seekMap.getSeekPoints(timeUs); + } +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/jpeg/StartOffsetExtractorOutput.java b/libraries/extractor/src/main/java/androidx/media3/extractor/jpeg/StartOffsetExtractorOutput.java index dd8aa3777e..23a0e91e80 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/jpeg/StartOffsetExtractorOutput.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/jpeg/StartOffsetExtractorOutput.java @@ -17,6 +17,7 @@ package androidx.media3.extractor.jpeg; import androidx.media3.common.util.UnstableApi; import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.extractor.ForwardingSeekMap; import androidx.media3.extractor.SeekMap; import androidx.media3.extractor.SeekPoint; import androidx.media3.extractor.TrackOutput; @@ -54,17 +55,7 @@ public final class StartOffsetExtractorOutput implements ExtractorOutput { @Override public void seekMap(SeekMap seekMap) { extractorOutput.seekMap( - new SeekMap() { - @Override - public boolean isSeekable() { - return seekMap.isSeekable(); - } - - @Override - public long getDurationUs() { - return seekMap.getDurationUs(); - } - + new ForwardingSeekMap(seekMap) { @Override public SeekPoints getSeekPoints(long timeUs) { SeekPoints seekPoints = seekMap.getSeekPoints(timeUs);