diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/BaseSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/BaseSamplePipeline.java index 3f7d88f3b7c..8f7f15cbdb9 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/BaseSamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/BaseSamplePipeline.java @@ -18,11 +18,13 @@ import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static java.lang.Math.max; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.Util; import androidx.media3.decoder.DecoderInputBuffer; import java.nio.ByteBuffer; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -38,6 +40,7 @@ @Nullable private DecoderInputBuffer inputBuffer; private boolean muxerWrapperTrackAdded; + private long currentPositionMs; private boolean isEnded; public BaseSamplePipeline( @@ -65,9 +68,11 @@ public DecoderInputBuffer dequeueInputBuffer() throws TransformationException { @Override public void queueInputBuffer() throws TransformationException { - checkNotNull(inputBuffer); + DecoderInputBuffer inputBuffer = checkNotNull(this.inputBuffer); + currentPositionMs = + max(currentPositionMs, Util.usToMs(inputBuffer.timeUs - streamStartPositionUs)); checkNotNull(inputBuffer.data); - if (!shouldDropInputBuffer()) { + if (!shouldDropInputBuffer(inputBuffer)) { queueInputBufferInternal(); } } @@ -82,6 +87,11 @@ public boolean isEnded() { return isEnded; } + @Override + public long getCurrentPositionMs() { + return currentPositionMs; + } + @Nullable protected abstract DecoderInputBuffer dequeueInputBufferInternal() throws TransformationException; @@ -103,8 +113,8 @@ public boolean isEnded() { * Preprocesses an {@linkplain DecoderInputBuffer input buffer} queued to the pipeline and returns * whether it should be dropped. */ - @RequiresNonNull({"inputBuffer", "inputBuffer.data"}) - private boolean shouldDropInputBuffer() { + @RequiresNonNull("#1.data") + private boolean shouldDropInputBuffer(DecoderInputBuffer inputBuffer) { ByteBuffer inputBytes = inputBuffer.data; if (sefVideoSlowMotionFlattener == null || inputBuffer.isEndOfStream()) { @@ -112,7 +122,6 @@ private boolean shouldDropInputBuffer() { } long presentationTimeUs = inputBuffer.timeUs - streamOffsetUs; - DecoderInputBuffer inputBuffer = this.inputBuffer; boolean shouldDropInputBuffer = sefVideoSlowMotionFlattener.dropOrTransformSample(inputBytes, presentationTimeUs); if (shouldDropInputBuffer) { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ExoPlayerAssetLoader.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ExoPlayerAssetLoader.java index b6a50ca5944..302539c8a1e 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ExoPlayerAssetLoader.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ExoPlayerAssetLoader.java @@ -21,11 +21,6 @@ import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_MAX_BUFFER_MS; import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_MIN_BUFFER_MS; -import static androidx.media3.transformer.Transformer.PROGRESS_STATE_AVAILABLE; -import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NO_TRANSFORMATION; -import static androidx.media3.transformer.Transformer.PROGRESS_STATE_UNAVAILABLE; -import static androidx.media3.transformer.Transformer.PROGRESS_STATE_WAITING_FOR_AVAILABILITY; -import static java.lang.Math.min; import android.content.Context; import android.os.Handler; @@ -39,6 +34,7 @@ import androidx.media3.common.Timeline; import androidx.media3.common.Tracks; import androidx.media3.common.util.Clock; +import androidx.media3.common.util.Util; import androidx.media3.exoplayer.DefaultLoadControl; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.Renderer; @@ -54,6 +50,8 @@ public interface Listener { + void onDurationMs(long durationMs); + void onTrackRegistered(); void onAllTracksRegistered(); @@ -74,7 +72,6 @@ SamplePipeline onTrackAdded(Format format, long streamStartPositionUs, long stre private final Clock clock; @Nullable private ExoPlayer player; - private @Transformer.ProgressState int progressState; public ExoPlayerAssetLoader( Context context, @@ -89,7 +86,6 @@ public ExoPlayerAssetLoader( this.mediaSourceFactory = mediaSourceFactory; this.looper = looper; this.clock = clock; - progressState = PROGRESS_STATE_NO_TRANSFORMATION; } public void start(MediaItem mediaItem, Listener listener) { @@ -125,22 +121,9 @@ public void start(MediaItem mediaItem, Listener listener) { player.setMediaItem(mediaItem); player.addListener(new PlayerListener(listener)); player.prepare(); - - progressState = PROGRESS_STATE_WAITING_FOR_AVAILABILITY; - } - - public @Transformer.ProgressState int getProgress(ProgressHolder progressHolder) { - if (progressState == PROGRESS_STATE_AVAILABLE) { - Player player = checkNotNull(this.player); - long durationMs = player.getDuration(); - long positionMs = player.getCurrentPosition(); - progressHolder.progress = min((int) (positionMs * 100 / durationMs), 99); - } - return progressState; } public void release() { - progressState = PROGRESS_STATE_NO_TRANSFORMATION; if (player != null) { player.release(); player = null; @@ -191,6 +174,7 @@ public Renderer[] createRenderers( private final class PlayerListener implements Player.Listener { private final Listener listener; + private boolean hasSentDuration; public PlayerListener(Listener listener) { this.listener = listener; @@ -205,20 +189,14 @@ public void onPlaybackStateChanged(int state) { @Override public void onTimelineChanged(Timeline timeline, int reason) { - if (progressState != PROGRESS_STATE_WAITING_FOR_AVAILABILITY) { + if (hasSentDuration) { return; } Timeline.Window window = new Timeline.Window(); timeline.getWindow(/* windowIndex= */ 0, window); if (!window.isPlaceholder) { - long durationUs = window.durationUs; - // Make progress permanently unavailable if the duration is unknown, so that it doesn't jump - // to a high value at the end of the transformation if the duration is set once the media is - // entirely loaded. - progressState = - durationUs <= 0 || durationUs == C.TIME_UNSET - ? PROGRESS_STATE_UNAVAILABLE - : PROGRESS_STATE_AVAILABLE; + listener.onDurationMs(Util.usToMs(window.durationUs)); + hasSentDuration = true; checkNotNull(player).play(); } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/SamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/SamplePipeline.java index 7ae4257c5ed..6c3bcf012db 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SamplePipeline.java @@ -49,4 +49,10 @@ /** Releases all resources held by the pipeline. */ void release(); + + /** + * Returns the current timestamp being processed in the track, in milliseconds. This is the + * largest timestamp queued minus the stream start time, or 0 if no input has been queued. + */ + long getCurrentPositionMs(); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java index 5377c0de6c6..c9df4e37d27 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java @@ -16,6 +16,12 @@ package androidx.media3.transformer; +import static androidx.media3.transformer.Transformer.PROGRESS_STATE_AVAILABLE; +import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NO_TRANSFORMATION; +import static androidx.media3.transformer.Transformer.PROGRESS_STATE_UNAVAILABLE; +import static androidx.media3.transformer.Transformer.PROGRESS_STATE_WAITING_FOR_AVAILABILITY; +import static java.lang.Math.min; + import android.content.Context; import android.os.Looper; import androidx.annotation.Nullable; @@ -32,6 +38,8 @@ import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.extractor.metadata.mp4.SlowMotionData; import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; /* package */ final class TransformerInternal { @@ -50,6 +58,10 @@ public interface Listener { private final FrameProcessor.Factory frameProcessorFactory; private final DebugViewProvider debugViewProvider; private final ExoPlayerAssetLoader exoPlayerAssetLoader; + private final List samplePipelines; + + private @Transformer.ProgressState int progressState; + private long durationMs; public TransformerInternal( Context context, @@ -74,6 +86,8 @@ public TransformerInternal( exoPlayerAssetLoader = new ExoPlayerAssetLoader( context, removeAudio, removeVideo, mediaSourceFactory, looper, clock); + samplePipelines = new ArrayList<>(/* initialCapacity= */ 2); + progressState = PROGRESS_STATE_NO_TRANSFORMATION; } public void start( @@ -84,16 +98,34 @@ public void start( AssetLoaderListener assetLoaderListener = new AssetLoaderListener(mediaItem, muxerWrapper, listener, fallbackListener); exoPlayerAssetLoader.start(mediaItem, assetLoaderListener); + progressState = PROGRESS_STATE_WAITING_FOR_AVAILABILITY; } public @Transformer.ProgressState int getProgress(ProgressHolder progressHolder) { - return exoPlayerAssetLoader.getProgress(progressHolder); + if (progressState == PROGRESS_STATE_AVAILABLE) { + long positionMs = getCurrentPositionMs(); + progressHolder.progress = min((int) (positionMs * 100 / durationMs), 99); + } + return progressState; } public void release() { + samplePipelines.clear(); + progressState = PROGRESS_STATE_NO_TRANSFORMATION; exoPlayerAssetLoader.release(); } + private long getCurrentPositionMs() { + if (samplePipelines.isEmpty()) { + return 0; + } + long positionMsSum = 0; + for (int i = 0; i < samplePipelines.size(); i++) { + positionMsSum += samplePipelines.get(i).getCurrentPositionMs(); + } + return positionMsSum / samplePipelines.size(); + } + private class AssetLoaderListener implements ExoPlayerAssetLoader.Listener { private final MediaItem mediaItem; @@ -114,6 +146,18 @@ public AssetLoaderListener( this.fallbackListener = fallbackListener; } + @Override + public void onDurationMs(long durationMs) { + // Make progress permanently unavailable if the duration is unknown, so that it doesn't jump + // to a high value at the end of the transformation if the duration is set once the media is + // entirely loaded. + progressState = + durationMs <= 0 || durationMs == C.TIME_UNSET + ? PROGRESS_STATE_UNAVAILABLE + : PROGRESS_STATE_AVAILABLE; + TransformerInternal.this.durationMs = durationMs; + } + @Override public void onTrackRegistered() { trackRegistered = true; @@ -132,7 +176,10 @@ public void onAllTracksRegistered() { public SamplePipeline onTrackAdded( Format format, long streamStartPositionUs, long streamOffsetUs) throws TransformationException { - return getSamplePipeline(format, streamStartPositionUs, streamOffsetUs); + SamplePipeline samplePipeline = + getSamplePipeline(format, streamStartPositionUs, streamOffsetUs); + samplePipelines.add(samplePipeline); + return samplePipeline; } @Override