From 7e12b9e15ffd184408418a8335d6acfa5de77209 Mon Sep 17 00:00:00 2001 From: tofunmi Date: Fri, 22 Dec 2023 07:35:06 -0800 Subject: [PATCH] Add rough progress updates in trim optimization PiperOrigin-RevId: 593116025 --- ...ansformerTrimOptimizationProgressTest.java | 243 ++++++++++++++++++ .../media3/transformer/Transformer.java | 87 +++++-- .../transformer/MediaItemExportTest.java | 125 ++++++++- 3 files changed, 432 insertions(+), 23 deletions(-) create mode 100644 libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerTrimOptimizationProgressTest.java diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerTrimOptimizationProgressTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerTrimOptimizationProgressTest.java new file mode 100644 index 0000000000..cdddc2526e --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerTrimOptimizationProgressTest.java @@ -0,0 +1,243 @@ +/* + * 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.transformer; + +import static androidx.media3.common.util.Util.isRunningOnEmulator; +import static androidx.media3.transformer.Transformer.PROGRESS_STATE_AVAILABLE; +import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NOT_STARTED; +import static androidx.media3.transformer.Transformer.PROGRESS_STATE_UNAVAILABLE; +import static androidx.media3.transformer.Transformer.PROGRESS_STATE_WAITING_FOR_AVAILABILITY; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +import android.content.Context; +import androidx.media3.common.MediaItem; +import androidx.media3.common.util.NullableType; +import androidx.media3.common.util.Util; +import androidx.media3.effect.DebugTraceUtil; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import com.google.common.collect.Iterables; +import com.google.common.util.concurrent.SettableFuture; +import java.io.File; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; + +/** + * End-to-end instrumentation test for {@link Transformer#getProgress} when {@link + * Transformer.Builder#experimentalSetTrimOptimizationEnabled} is enabled. + */ +@RunWith(AndroidJUnit4.class) +public class TransformerTrimOptimizationProgressTest { + @Rule public final TestName testName = new TestName(); + + private final Context context = ApplicationProvider.getApplicationContext(); + + private @MonotonicNonNull String testId; + + @Before + @EnsuresNonNull({"testId"}) + public void setUp() { + testId = testName.getMethodName(); + } + + @Test + @RequiresNonNull("testId") + public void getProgress_trimOptimizationEnabledAndApplied_givesIncreasingPercentages() + throws Exception { + // The trim optimization is only guaranteed to work on emulator for this file. + assumeTrue(isRunningOnEmulator()); + // MediaCodec returns a segmentation fault fails at this SDK level on emulators. + assumeFalse(Util.SDK_INT == 26); + Transformer transformer = + new Transformer.Builder(context).experimentalSetTrimOptimizationEnabled(true).build(); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri("asset:///media/mp4/internal_emulator_transformer_output.mp4") + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder() + .setStartPositionMs(500) + .setEndPositionMs(2500) + .build()) + .build(); + // Written to on main sync, read on test thread. + Queue progresses = new ConcurrentLinkedDeque<>(); + SettableFuture<@NullableType Exception> transformerExceptionFuture = SettableFuture.create(); + DebugTraceUtil.enableTracing = true; + // Created on test thread, only used on main sync. + Transformer testTransformer = + transformer + .buildUpon() + .addListener( + new Transformer.Listener() { + @Override + public void onCompleted(Composition composition, ExportResult exportResult) { + transformerExceptionFuture.set(null); + } + + @Override + public void onError( + Composition composition, + ExportResult exportResult, + ExportException exportException) { + transformerExceptionFuture.set(exportException); + } + }) + .build(); + File outputVideoFile = + AndroidTestUtil.createExternalCacheFile(context, /* fileName= */ testId + "-output.mp4"); + + InstrumentationRegistry.getInstrumentation() + .runOnMainSync( + () -> { + try { + testTransformer.start(mediaItem, outputVideoFile.getAbsolutePath()); + // Catch all exceptions to report. Exceptions thrown here that are not caught will + // NOT propagate. + } catch (RuntimeException e) { + transformerExceptionFuture.set(e); + } + }); + while (!transformerExceptionFuture.isDone()) { + InstrumentationRegistry.getInstrumentation() + .runOnMainSync( + () -> { + ProgressHolder progressHolder = new ProgressHolder(); + if (testTransformer.getProgress(progressHolder) == PROGRESS_STATE_AVAILABLE + && (progresses.isEmpty() + || Iterables.getLast(progresses) != progressHolder.progress)) { + progresses.add(progressHolder.progress); + } + }); + Thread.sleep(/* millis= */ 200); + } + + assertThat(transformerExceptionFuture.get()).isNull(); + assertThat(progresses).isInOrder(); + if (!progresses.isEmpty()) { + // The progress list could be empty if the export ends before any progress can be retrieved. + assertThat(Iterables.getFirst(progresses, /* defaultValue= */ -1)).isAtLeast(0); + assertThat(Iterables.getLast(progresses)).isAtMost(100); + } + } + + @Test + @RequiresNonNull("testId") + public void getProgress_trimOptimizationEnabledAndActive_returnsConsistentStates() + throws Exception { + // The trim optimization is only guaranteed to work on emulator for this file. + assumeTrue(isRunningOnEmulator()); + // MediaCodec returns a segmentation fault fails at this SDK level on emulators. + assumeFalse(Util.SDK_INT == 26); + Transformer transformer = + new Transformer.Builder(context).experimentalSetTrimOptimizationEnabled(true).build(); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri("asset:///media/mp4/internal_emulator_transformer_output.mp4") + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder() + .setStartPositionMs(500) + .setEndPositionMs(2500) + .build()) + .build(); + AtomicInteger previousProgressState = + new AtomicInteger(PROGRESS_STATE_WAITING_FOR_AVAILABILITY); + AtomicBoolean foundInconsistentState = new AtomicBoolean(); + SettableFuture<@NullableType Exception> transformerExceptionFuture = SettableFuture.create(); + DebugTraceUtil.enableTracing = true; + // Created on test thread, only used on main sync. + Transformer testTransformer = + transformer + .buildUpon() + .addListener( + new Transformer.Listener() { + @Override + public void onCompleted(Composition composition, ExportResult exportResult) { + transformerExceptionFuture.set(null); + } + + @Override + public void onError( + Composition composition, + ExportResult exportResult, + ExportException exportException) { + transformerExceptionFuture.set(exportException); + } + }) + .build(); + File outputVideoFile = + AndroidTestUtil.createExternalCacheFile(context, /* fileName= */ testId + "-output.mp4"); + + InstrumentationRegistry.getInstrumentation() + .runOnMainSync( + () -> { + try { + testTransformer.start(mediaItem, outputVideoFile.getAbsolutePath()); + // Catch all exceptions to report. Exceptions thrown here that are not caught will + // NOT propagate. + } catch (RuntimeException e) { + transformerExceptionFuture.set(e); + } + }); + while (!transformerExceptionFuture.isDone()) { + InstrumentationRegistry.getInstrumentation() + .runOnMainSync( + () -> { + @Transformer.ProgressState + int progressState = transformer.getProgress(new ProgressHolder()); + if (progressState == PROGRESS_STATE_UNAVAILABLE) { + foundInconsistentState.set(true); + return; + } + switch (previousProgressState.get()) { + case PROGRESS_STATE_WAITING_FOR_AVAILABILITY: + break; + case PROGRESS_STATE_AVAILABLE: + if (progressState == PROGRESS_STATE_WAITING_FOR_AVAILABILITY) { + foundInconsistentState.set(true); + return; + } + break; + case PROGRESS_STATE_NOT_STARTED: + if (progressState != PROGRESS_STATE_NOT_STARTED) { + foundInconsistentState.set(true); + return; + } + break; + default: + throw new IllegalStateException(); + } + previousProgressState.set(progressState); + }); + Thread.sleep(/* millis= */ 200); + } + + assertThat(transformerExceptionFuture.get()).isNull(); + assertThat(foundInconsistentState.get()).isFalse(); + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java index 86503091f6..026e6e5b8d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java @@ -28,6 +28,7 @@ import static androidx.media3.transformer.ExportResult.OPTIMIZATION_FAILED_FORMA import static androidx.media3.transformer.TransformerUtil.shouldTranscodeAudio; import static androidx.media3.transformer.TransformerUtil.shouldTranscodeVideo; import static androidx.media3.transformer.TransmuxTranscodeHelper.buildNewCompositionWithClipTimes; +import static java.lang.Math.round; import static java.lang.annotation.ElementType.TYPE_USE; import android.content.Context; @@ -294,7 +295,6 @@ public final class Transformer { return this; } - // TODO: b/304476154 - Support progress updates in trim optimization. /** * Sets whether to attempt to optimize trims from the start of the {@link EditedMediaItem} by * transcoding as little of the file as possible and transmuxing the rest. @@ -305,7 +305,6 @@ public final class Transformer { *
  • Only supported for single-asset (i.e. only one {@link EditedMediaItem} in the whole * {@link Composition}) exports of mp4 files. *
  • Not guaranteed to work with any effects. - *
  • Progress updates will be unavailable. * * *

    This process relies on the given {@linkplain #setEncoderFactory EncoderFactory} providing @@ -781,7 +780,7 @@ public final class Transformer { getResumeMetadataFuture; private @MonotonicNonNull ListenableFuture copyOutputFuture; private @MonotonicNonNull ListenableFuture getMp4MetadataInfoFuture; - private @MonotonicNonNull Mp4MetadataInfo mp4MetadataInfo; + @Nullable private Mp4MetadataInfo mediaItemMetadataInfo; private Transformer( Context context, @@ -821,6 +820,7 @@ public final class Transformer { this.looper = looper; this.debugViewProvider = debugViewProvider; this.clock = clock; + transformerState = TRANSFORMER_STATE_PROCESS_FULL_INPUT; applicationHandler = clock.createHandler(looper, /* callback= */ null); componentListener = new ComponentListener(); exportResultBuilder = new ExportResult.Builder(); @@ -1069,12 +1069,65 @@ public final class Transformer { */ public @ProgressState int getProgress(ProgressHolder progressHolder) { verifyApplicationThread(); - if (transformerState != TRANSFORMER_STATE_PROCESS_FULL_INPUT) { + if (transformerState == TRANSFORMER_STATE_PROCESS_FULL_INPUT) { + return transformerInternal == null + ? PROGRESS_STATE_NOT_STARTED + : transformerInternal.getProgress(progressHolder); + } + if (isExportResumed()) { return PROGRESS_STATE_UNAVAILABLE; } - return transformerInternal == null - ? PROGRESS_STATE_NOT_STARTED - : transformerInternal.getProgress(progressHolder); + + return getTrimOptimizationProgress(progressHolder); + } + + private boolean isExportResumed() { + return transformerState == TRANSFORMER_STATE_REMUX_PROCESSED_VIDEO + || transformerState == TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO + || transformerState == TRANSFORMER_STATE_PROCESS_AUDIO + || transformerState == TRANSFORMER_STATE_COPY_OUTPUT; + } + + private @ProgressState int getTrimOptimizationProgress(ProgressHolder progressHolder) { + if (mediaItemMetadataInfo == null) { + return PROGRESS_STATE_WAITING_FOR_AVAILABILITY; + } + MediaItem firstMediaItem = + checkNotNull(composition).sequences.get(0).editedMediaItems.get(0).mediaItem; + long trimStartTimeUs = firstMediaItem.clippingConfiguration.startPositionUs; + long transcodeDuration = + mediaItemMetadataInfo.firstSyncSampleTimestampUsAfterTimeUs - trimStartTimeUs; + float transcodeWeighting = (float) transcodeDuration / mediaItemMetadataInfo.durationUs; + + if (transformerState == TRANSFORMER_STATE_PROCESS_MEDIA_START) { + if (transformerInternal == null) { + return PROGRESS_STATE_WAITING_FOR_AVAILABILITY; + } + @ProgressState + int processMediaStartProgressState = transformerInternal.getProgress(progressHolder); + if (processMediaStartProgressState == PROGRESS_STATE_AVAILABLE) { + progressHolder.progress = round(progressHolder.progress * transcodeWeighting); + } + return processMediaStartProgressState; + } + + float fullTranscodeProgress = 100 * transcodeWeighting; + if (transformerInternal == null) { + // Transformer has not started remuxing the remaining media yet. + progressHolder.progress = round(fullTranscodeProgress); + return PROGRESS_STATE_AVAILABLE; + } + @ProgressState + int remuxRemainingMediaProgressState = transformerInternal.getProgress(progressHolder); + if (remuxRemainingMediaProgressState == PROGRESS_STATE_NOT_STARTED + || remuxRemainingMediaProgressState == PROGRESS_STATE_WAITING_FOR_AVAILABILITY) { + progressHolder.progress = round(fullTranscodeProgress); + return PROGRESS_STATE_AVAILABLE; + } else if (remuxRemainingMediaProgressState == PROGRESS_STATE_AVAILABLE) { + progressHolder.progress = + round(fullTranscodeProgress + (1 - transcodeWeighting) * progressHolder.progress); + } + return remuxRemainingMediaProgressState; } /** @@ -1329,7 +1382,7 @@ public final class Transformer { processFullInput(); return; } - Transformer.this.mp4MetadataInfo = mp4MetadataInfo; + Transformer.this.mediaItemMetadataInfo = mp4MetadataInfo; Composition trancodeComposition = buildNewCompositionWithClipTimes( composition, @@ -1355,6 +1408,7 @@ public final class Transformer { } private void remuxRemainingMedia() { + Mp4MetadataInfo mediaItemMetadataInfo = checkNotNull(this.mediaItemMetadataInfo); transformerState = TRANSFORMER_STATE_REMUX_REMAINING_MEDIA; if (!doesFormatsMatch()) { remuxingMuxerWrapper = null; @@ -1367,13 +1421,12 @@ public final class Transformer { checkNotNull(composition).sequences.get(0).editedMediaItems.get(0).mediaItem; long trimStartTimeUs = firstMediaItem.clippingConfiguration.startPositionUs; long trimEndTimeUs = firstMediaItem.clippingConfiguration.endPositionUs; - checkNotNull(mp4MetadataInfo); Composition transmuxComposition = buildNewCompositionWithClipTimes( composition, - mp4MetadataInfo.firstSyncSampleTimestampUsAfterTimeUs, + mediaItemMetadataInfo.firstSyncSampleTimestampUsAfterTimeUs, trimEndTimeUs, - mp4MetadataInfo.durationUs, + mediaItemMetadataInfo.durationUs, /* startsAtKeyFrame= */ true); checkNotNull(remuxingMuxerWrapper); remuxingMuxerWrapper.changeToAppendMode(); @@ -1381,19 +1434,19 @@ public final class Transformer { transmuxComposition, remuxingMuxerWrapper, componentListener, - /* initialTimestampOffsetUs= */ mp4MetadataInfo.firstSyncSampleTimestampUsAfterTimeUs + /* initialTimestampOffsetUs= */ mediaItemMetadataInfo.firstSyncSampleTimestampUsAfterTimeUs - trimStartTimeUs); } private boolean doesFormatsMatch() { - checkNotNull(mp4MetadataInfo); + Mp4MetadataInfo mediaItemMetadataInfo = checkNotNull(this.mediaItemMetadataInfo); boolean videoFormatMatches = checkNotNull(remuxingMuxerWrapper) .getTrackFormat(C.TRACK_TYPE_VIDEO) - .initializationDataEquals(checkNotNull(mp4MetadataInfo.videoFormat)); + .initializationDataEquals(checkNotNull(mediaItemMetadataInfo.videoFormat)); boolean audioFormatMatches = - mp4MetadataInfo.audioFormat == null - || mp4MetadataInfo.audioFormat.initializationDataEquals( + mediaItemMetadataInfo.audioFormat == null + || mediaItemMetadataInfo.audioFormat.initializationDataEquals( checkNotNull(remuxingMuxerWrapper).getTrackFormat(C.TRACK_TYPE_AUDIO)); return videoFormatMatches && audioFormatMatches; } @@ -1501,6 +1554,8 @@ public final class Transformer { } else if (transformerState == TRANSFORMER_STATE_PROCESS_MEDIA_START) { remuxRemainingMedia(); } else if (transformerState == TRANSFORMER_STATE_REMUX_REMAINING_MEDIA) { + transformerState = TRANSFORMER_STATE_PROCESS_FULL_INPUT; + mediaItemMetadataInfo = null; exportResultBuilder.setOptimizationResult(ExportResult.OPTIMIZATION_SUCCEEDED); onExportCompletedWithSuccess(); } else { diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java index 0b47d162c3..169e12637d 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java @@ -1065,6 +1065,51 @@ public final class MediaItemExportTest { /* originalFileName= */ FILE_AUDIO_VIDEO, /* modifications...= */ "rotated")); } + @Test + public void getProgress_unknownDuration_returnsConsistentStates() throws Exception { + Transformer transformer = + createTransformerBuilder(muxerFactory, /* enableFallback= */ false).build(); + MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_UNKNOWN_DURATION); + AtomicInteger previousProgressState = + new AtomicInteger(PROGRESS_STATE_WAITING_FOR_AVAILABILITY); + AtomicBoolean foundInconsistentState = new AtomicBoolean(); + Handler progressHandler = + new Handler(Looper.myLooper()) { + @Override + public void handleMessage(Message msg) { + @Transformer.ProgressState + int progressState = transformer.getProgress(new ProgressHolder()); + switch (previousProgressState.get()) { + case PROGRESS_STATE_WAITING_FOR_AVAILABILITY: + break; + case PROGRESS_STATE_UNAVAILABLE: + case PROGRESS_STATE_AVAILABLE: // See [Internal: b/176145097]. + if (progressState == PROGRESS_STATE_WAITING_FOR_AVAILABILITY) { + foundInconsistentState.set(true); + return; + } + break; + case PROGRESS_STATE_NOT_STARTED: + if (progressState != PROGRESS_STATE_NOT_STARTED) { + foundInconsistentState.set(true); + return; + } + break; + default: + throw new IllegalStateException(); + } + previousProgressState.set(progressState); + sendEmptyMessage(0); + } + }; + + transformer.start(mediaItem, outputDir.newFile().getPath()); + progressHandler.sendEmptyMessage(0); + TransformerTestRunner.runLooper(transformer); + + assertThat(foundInconsistentState.get()).isFalse(); + } + @Test public void getProgress_knownDuration_returnsConsistentStates() throws Exception { Transformer transformer = @@ -1165,10 +1210,14 @@ public final class MediaItemExportTest { } @Test - public void getProgress_unknownDuration_returnsConsistentStates() throws Exception { + public void + getProgress_trimOptimizationEnabledButNotApplied_withClippingConfigurationUnset_returnsConsistentStates() + throws Exception { Transformer transformer = - createTransformerBuilder(muxerFactory, /* enableFallback= */ false).build(); - MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_UNKNOWN_DURATION); + createTransformerBuilder(muxerFactory, /* enableFallback= */ false) + .experimentalSetTrimOptimizationEnabled(true) + .build(); + MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY); AtomicInteger previousProgressState = new AtomicInteger(PROGRESS_STATE_WAITING_FOR_AVAILABILITY); AtomicBoolean foundInconsistentState = new AtomicBoolean(); @@ -1178,11 +1227,14 @@ public final class MediaItemExportTest { public void handleMessage(Message msg) { @Transformer.ProgressState int progressState = transformer.getProgress(new ProgressHolder()); + if (progressState == PROGRESS_STATE_UNAVAILABLE) { + foundInconsistentState.set(true); + return; + } switch (previousProgressState.get()) { case PROGRESS_STATE_WAITING_FOR_AVAILABILITY: break; - case PROGRESS_STATE_UNAVAILABLE: - case PROGRESS_STATE_AVAILABLE: // See [Internal: b/176145097]. + case PROGRESS_STATE_AVAILABLE: if (progressState == PROGRESS_STATE_WAITING_FOR_AVAILABILITY) { foundInconsistentState.set(true); return; @@ -1198,17 +1250,76 @@ public final class MediaItemExportTest { throw new IllegalStateException(); } previousProgressState.set(progressState); - sendEmptyMessage(0); + sendEmptyMessageDelayed(/* what= */ 0, /* delayMillis= */ 50); } }; transformer.start(mediaItem, outputDir.newFile().getPath()); - progressHandler.sendEmptyMessage(0); + progressHandler.sendEmptyMessage(/* what= */ 0); TransformerTestRunner.runLooper(transformer); assertThat(foundInconsistentState.get()).isFalse(); } + @Test + public void + getProgress_trimOptimizationEnabledButNotApplied_withClippingConfigurationUnset_givesIncreasingPercentages() + throws Exception { + Transformer transformer = + createTransformerBuilder(muxerFactory, /* enableFallback= */ false) + .experimentalSetTrimOptimizationEnabled(true) + .build(); + MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY); + List progresses = new ArrayList<>(); + Handler progressHandler = + new Handler(Looper.myLooper()) { + @Override + public void handleMessage(Message msg) { + ProgressHolder progressHolder = new ProgressHolder(); + @Transformer.ProgressState int progressState = transformer.getProgress(progressHolder); + if (progressState == PROGRESS_STATE_NOT_STARTED) { + return; + } + if (progressState == PROGRESS_STATE_AVAILABLE + && (progresses.isEmpty() + || Iterables.getLast(progresses) != progressHolder.progress)) { + progresses.add(progressHolder.progress); + } + sendEmptyMessageDelayed(/* what= */ 0, /* delayMillis= */ 50); + } + }; + + transformer.start(mediaItem, outputDir.newFile().getPath()); + progressHandler.sendEmptyMessage(/* what= */ 0); + TransformerTestRunner.runLooper(transformer); + + assertThat(progresses).isInOrder(); + if (!progresses.isEmpty()) { + // The progress list could be empty if the export ends before any progress can be retrieved. + assertThat(progresses.get(0)).isAtLeast(0); + assertThat(Iterables.getLast(progresses)).isAtMost(100); + } + } + + @Test + public void + getProgress_trimOptimizationEnabledButNotApplied_withClippingConfigurationUnset_noCurrentExport_returnsNotStarted() + throws Exception { + Transformer transformer = + createTransformerBuilder(muxerFactory, /* enableFallback= */ false) + .experimentalSetTrimOptimizationEnabled(true) + .build(); + MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY); + ProgressHolder progressHolder = new ProgressHolder(); + @Transformer.ProgressState int stateBeforeTransform = transformer.getProgress(progressHolder); + transformer.start(mediaItem, outputDir.newFile().getPath()); + TransformerTestRunner.runLooper(transformer); + @Transformer.ProgressState int stateAfterTransform = transformer.getProgress(progressHolder); + + assertThat(stateBeforeTransform).isEqualTo(PROGRESS_STATE_NOT_STARTED); + assertThat(stateAfterTransform).isEqualTo(PROGRESS_STATE_NOT_STARTED); + } + @Test public void getProgress_fromWrongThread_throwsError() throws Exception { Transformer transformer =