Make progress tests stricter.

These changes are possible because getProgress is no longer a blocking
operation on transformer.

* Tests call getProgress after every looper message executed.
* Use longer media assets for getProgress tests to give more progress
  intervals.
* Remove conditional assertions.

PiperOrigin-RevId: 639734368
This commit is contained in:
samrobinson 2024-06-03 04:40:03 -07:00 committed by Copybara-Service
parent 2bb12de00d
commit 2c916dc306
2 changed files with 157 additions and 218 deletions

View File

@ -62,7 +62,7 @@ import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.util.Pair;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
@ -94,11 +94,12 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@ -1327,44 +1328,20 @@ public final class MediaItemExportTest {
Transformer transformer = Transformer transformer =
createTransformerBuilder(muxerFactory, /* enableFallback= */ false).build(); createTransformerBuilder(muxerFactory, /* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_UNKNOWN_DURATION); 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()); transformer.start(mediaItem, outputDir.newFile().getPath());
progressHandler.sendEmptyMessage(0); Pair<ImmutableList<@Transformer.ProgressState Integer>, ImmutableList<Integer>>
TransformerTestRunner.runLooper(transformer); progressStatesAndValues = runTransformerForProgressStateAndValueUpdates(transformer);
ImmutableList<@Transformer.ProgressState Integer> progressStates =
progressStatesAndValues.first;
assertThat(foundInconsistentState.get()).isFalse(); assertThat(progressStates).isNotEmpty();
assertThat(progressStates)
.containsExactly(
PROGRESS_STATE_WAITING_FOR_AVAILABILITY,
PROGRESS_STATE_UNAVAILABLE,
PROGRESS_STATE_NOT_STARTED)
.inOrder();
} }
@Test @Test
@ -1372,48 +1349,21 @@ public final class MediaItemExportTest {
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false); CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false);
Transformer transformer = Transformer transformer =
createTransformerBuilder(muxerFactory, /* enableFallback= */ false).build(); createTransformerBuilder(muxerFactory, /* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY); MediaItem mediaItem =
AtomicInteger previousProgressState = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S);
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());
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);
sendEmptyMessage(0);
}
};
transformer.start(mediaItem, outputDir.newFile().getPath()); transformer.start(mediaItem, outputDir.newFile().getPath());
progressHandler.sendEmptyMessage(0); Pair<ImmutableList<@Transformer.ProgressState Integer>, ImmutableList<Integer>>
TransformerTestRunner.runLooper(transformer); progressStatesAndValues = runTransformerForProgressStateAndValueUpdates(transformer);
ImmutableList<@Transformer.ProgressState Integer> progressStates =
progressStatesAndValues.first;
assertThat(foundInconsistentState.get()).isFalse(); assertThat(progressStates)
.containsExactly(
PROGRESS_STATE_WAITING_FOR_AVAILABILITY,
PROGRESS_STATE_AVAILABLE,
PROGRESS_STATE_NOT_STARTED)
.inOrder();
} }
@Test @Test
@ -1421,36 +1371,18 @@ public final class MediaItemExportTest {
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false); CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false);
Transformer transformer = Transformer transformer =
createTransformerBuilder(muxerFactory, /* enableFallback= */ false).build(); createTransformerBuilder(muxerFactory, /* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY); MediaItem mediaItem =
List<Integer> progresses = new ArrayList<>(); MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S);
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_WAITING_FOR_AVAILABILITY
&& (progresses.isEmpty()
|| Iterables.getLast(progresses) != progressHolder.progress)) {
progresses.add(progressHolder.progress);
}
sendEmptyMessage(0);
}
};
transformer.start(mediaItem, outputDir.newFile().getPath()); transformer.start(mediaItem, outputDir.newFile().getPath());
progressHandler.sendEmptyMessage(0); Pair<ImmutableList<@Transformer.ProgressState Integer>, ImmutableList<Integer>>
TransformerTestRunner.runLooper(transformer); progressStatesAndValues = runTransformerForProgressStateAndValueUpdates(transformer);
ImmutableList<Integer> progressValues = progressStatesAndValues.second;
assertThat(progresses).isInOrder(); assertThat(progressValues).isNotEmpty();
if (!progresses.isEmpty()) { assertThat(progressValues.get(0)).isEqualTo(0);
// The progress list could be empty if the export ends before any progress can be retrieved. assertThat(progressValues).isInStrictOrder();
assertThat(progresses.get(0)).isAtLeast(0); assertThat(Iterables.getLast(progressValues)).isAtMost(100);
assertThat(Iterables.getLast(progresses)).isLessThan(100);
}
} }
@Test @Test
@ -1469,120 +1401,6 @@ public final class MediaItemExportTest {
assertThat(stateAfterTransform).isEqualTo(PROGRESS_STATE_NOT_STARTED); assertThat(stateAfterTransform).isEqualTo(PROGRESS_STATE_NOT_STARTED);
} }
@Test
public void
getProgress_trimOptimizationEnabledButNotApplied_withClippingConfigurationUnset_returnsConsistentStates()
throws Exception {
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false);
Transformer transformer =
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();
Handler progressHandler =
new Handler(Looper.myLooper()) {
@Override
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_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);
sendEmptyMessageDelayed(/* what= */ 0, /* delayMillis= */ 50);
}
};
transformer.start(mediaItem, outputDir.newFile().getPath());
progressHandler.sendEmptyMessage(/* what= */ 0);
TransformerTestRunner.runLooper(transformer);
assertThat(foundInconsistentState.get()).isFalse();
}
@Test
public void
getProgress_trimOptimizationEnabledButNotApplied_withClippingConfigurationUnset_givesIncreasingPercentages()
throws Exception {
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false);
Transformer transformer =
createTransformerBuilder(muxerFactory, /* enableFallback= */ false)
.experimentalSetTrimOptimizationEnabled(true)
.build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY);
List<Integer> 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 {
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false);
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 @Test
public void getProgress_fromWrongThread_throwsError() throws Exception { public void getProgress_fromWrongThread_throwsError() throws Exception {
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false); CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false);
@ -1609,6 +1427,75 @@ public final class MediaItemExportTest {
assertThat(illegalStateException.get()).isNotNull(); assertThat(illegalStateException.get()).isNotNull();
} }
@Test
public void
getProgress_trimOptimizationEnabledButNotApplied_withClippingConfigurationUnset_givesIncreasingPercentages()
throws Exception {
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false);
Transformer transformer =
createTransformerBuilder(muxerFactory, /* enableFallback= */ false)
.experimentalSetTrimOptimizationEnabled(true)
.build();
MediaItem mediaItem =
MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S);
transformer.start(mediaItem, outputDir.newFile().getPath());
Pair<ImmutableList<@Transformer.ProgressState Integer>, ImmutableList<Integer>>
progressStatesAndValues = runTransformerForProgressStateAndValueUpdates(transformer);
ImmutableList<Integer> progressValues = progressStatesAndValues.second;
assertThat(progressValues).isNotEmpty();
assertThat(progressValues.get(0)).isEqualTo(0);
assertThat(progressValues).isInStrictOrder();
assertThat(Iterables.getLast(progressValues)).isAtMost(100);
}
@Test
public void
getProgress_trimOptimizationEnabledButNotApplied_withClippingConfigurationUnset_returnsConsistentStates()
throws Exception {
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false);
Transformer transformer =
createTransformerBuilder(muxerFactory, /* enableFallback= */ false)
.experimentalSetTrimOptimizationEnabled(true)
.build();
MediaItem mediaItem =
MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S);
transformer.start(mediaItem, outputDir.newFile().getPath());
Pair<ImmutableList<@Transformer.ProgressState Integer>, ImmutableList<Integer>>
progressStatesAndValues = runTransformerForProgressStateAndValueUpdates(transformer);
ImmutableList<@Transformer.ProgressState Integer> progressStates =
progressStatesAndValues.first;
assertThat(progressStates)
.containsExactly(
PROGRESS_STATE_WAITING_FOR_AVAILABILITY,
PROGRESS_STATE_AVAILABLE,
PROGRESS_STATE_NOT_STARTED)
.inOrder();
}
@Test
public void
getProgress_trimOptimizationEnabledButNotApplied_withClippingConfigurationUnset_noCurrentExport_returnsNotStarted()
throws Exception {
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false);
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 @Test
public void cancel_afterCompletion_doesNotThrow() throws Exception { public void cancel_afterCompletion_doesNotThrow() throws Exception {
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false); CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false);
@ -1755,6 +1642,32 @@ public final class MediaItemExportTest {
}); });
} }
private Pair<ImmutableList<@Transformer.ProgressState Integer>, ImmutableList<Integer>>
runTransformerForProgressStateAndValueUpdates(Transformer transformer)
throws ExportException, TimeoutException {
ConcurrentLinkedDeque<@Transformer.ProgressState Integer> progressStates =
new ConcurrentLinkedDeque<>();
ConcurrentLinkedDeque<Integer> progressValues = new ConcurrentLinkedDeque<>();
ProgressHolder progressHolder = new ProgressHolder();
TransformerTestRunner.runLooperWithListener(
transformer,
() -> {
@Transformer.ProgressState int progressState = transformer.getProgress(progressHolder);
if (progressStates.isEmpty() || progressState != progressStates.getLast()) {
progressStates.add(progressState);
}
if (progressState == PROGRESS_STATE_AVAILABLE
&& (progressValues.isEmpty()
|| progressHolder.progress != progressValues.getLast())) {
progressValues.add(progressHolder.progress);
}
});
return new Pair<>(ImmutableList.copyOf(progressStates), ImmutableList.copyOf(progressValues));
}
private static final class SlowExtractorsFactory implements ExtractorsFactory { private static final class SlowExtractorsFactory implements ExtractorsFactory {
private final long delayBetweenReadsMs; private final long delayBetweenReadsMs;

View File

@ -43,6 +43,24 @@ public final class TransformerTestRunner {
*/ */
public static ExportResult runLooper(Transformer transformer) public static ExportResult runLooper(Transformer transformer)
throws ExportException, TimeoutException { throws ExportException, TimeoutException {
return runLooperWithListener(transformer, () -> {});
}
/**
* Runs tasks of the {@linkplain Transformer#getApplicationLooper() transformer Looper} until the
* {@linkplain Transformer export} ends.
*
* @param transformer The {@link Transformer}.
* @param beforeLooperTaskListener The {@link Runnable} to {@linkplain Runnable#run() run} before
* each looper task.
* @return The {@link ExportResult}.
* @throws ExportException If the export threw an exception.
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
* exceeded.
*/
public static ExportResult runLooperWithListener(
Transformer transformer, Runnable beforeLooperTaskListener)
throws ExportException, TimeoutException {
AtomicReference<@NullableType ExportResult> exportResultRef = new AtomicReference<>(); AtomicReference<@NullableType ExportResult> exportResultRef = new AtomicReference<>();
transformer.addListener( transformer.addListener(
@ -61,7 +79,15 @@ public final class TransformerTestRunner {
exportResultRef.set(exportResult); exportResultRef.set(exportResult);
} }
}); });
runLooperUntil(transformer.getApplicationLooper(), () -> exportResultRef.get() != null); runLooperUntil(
transformer.getApplicationLooper(),
() -> {
if (exportResultRef.get() != null) {
return true;
}
beforeLooperTaskListener.run();
return false;
});
ExportResult exportResult = checkNotNull(exportResultRef.get()); ExportResult exportResult = checkNotNull(exportResultRef.get());
if (exportResult.exportException != null) { if (exportResult.exportException != null) {