diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/ParameterizedAndroidTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/ParameterizedAndroidTestUtil.java new file mode 100644 index 0000000000..aa317d30fe --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/ParameterizedAndroidTestUtil.java @@ -0,0 +1,201 @@ +/* + * Copyright 2024 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.MimeTypes.VIDEO_H265; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.transformer.AndroidTestUtil.assumeFormatsSupported; + +import android.content.Context; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.ColorInfo; +import androidx.media3.common.Format; +import androidx.media3.common.MediaItem; +import androidx.media3.common.util.Util; +import androidx.media3.transformer.AndroidTestUtil.AssetInfo; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import java.math.RoundingMode; +import java.util.Objects; + +/** Test utilities for parameterized instrumentation tests. */ +/* package */ final class ParameterizedAndroidTestUtil { + + /** + * Assume that the device supports the inputs and output of the sequence. + * + *

See {@link AndroidTestUtil#assumeFormatsSupported(Context, String, Format, Format)}. + * + * @param context The {@link Context context}. + * @param testId The test ID. + * @param sequence The {@link SequenceConfig}. + * @throws Exception If an error occurs checking device support. + */ + public static void assumeSequenceFormatsSupported( + Context context, String testId, SequenceConfig sequence) throws Exception { + checkState(!sequence.itemConfigs.isEmpty()); + Format outputFormat = checkNotNull(sequence.itemConfigs.get(0).outputFormat); + for (ItemConfig item : sequence.itemConfigs) { + assumeFormatsSupported(context, testId, item.format, outputFormat); + } + } + + /** Test parameters for an {@link EditedMediaItemSequence}. */ + public static final class SequenceConfig { + public final int totalExpectedFrameCount; + public final ImmutableList itemConfigs; + + public SequenceConfig(ItemConfig... itemConfigs) { + this.itemConfigs = ImmutableList.copyOf(itemConfigs); + int frameCountSum = 0; + for (ItemConfig item : itemConfigs) { + frameCountSum += item.frameCount; + } + this.totalExpectedFrameCount = frameCountSum; + } + + /** Builds a {@link Composition} from the sequence configuration. */ + public Composition buildComposition(Effects compositionEffects) { + ImmutableList.Builder editedMediaItems = new ImmutableList.Builder<>(); + for (ItemConfig itemConfig : itemConfigs) { + editedMediaItems.add(itemConfig.build()); + } + + return new Composition.Builder(new EditedMediaItemSequence(editedMediaItems.build())) + .setEffects(compositionEffects) + .build(); + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("Seq{"); + for (ItemConfig itemConfig : itemConfigs) { + stringBuilder.append(itemConfig).append(","); + } + stringBuilder.replace(stringBuilder.length() - 1, stringBuilder.length(), "}"); + return stringBuilder.toString(); + } + } + + /** Test parameters for an {@link EditedMediaItem}. */ + public abstract static class ItemConfig { + public final int frameCount; + @Nullable public final Format format; + @Nullable public final Format outputFormat; + + protected final Effects effects; + + private final String uri; + + public ItemConfig( + String uri, + int frameCount, + @Nullable Format format, + @Nullable Format outputFormat, + Effects effects) { + this.uri = uri; + this.frameCount = frameCount; + this.format = format; + this.outputFormat = outputFormat; + this.effects = effects; + } + + public final EditedMediaItem build() { + EditedMediaItem.Builder builder = + new EditedMediaItem.Builder(MediaItem.fromUri(uri)).setEffects(effects); + onBuild(builder); + return builder.build(); + } + + /** + * Called when an {@link EditedMediaItem} is being {@linkplain #build() built}. + * + * @param builder The {@link EditedMediaItem.Builder} to optionally modify before the item is + * built. + */ + protected abstract void onBuild(EditedMediaItem.Builder builder); + + @Override + public String toString() { + return Iterables.getLast(Splitter.on("/").splitToList(uri)) + + (Objects.equals(effects, Effects.EMPTY) ? "" : "-effects"); + } + } + + /** {@link ItemConfig} for an SDR image {@link EditedMediaItem}. */ + public static final class SdrImageItemConfig extends ItemConfig { + + private final int frameRate; + private final long durationUs; + + public SdrImageItemConfig(AssetInfo assetInfo, int frameCount) { + this(assetInfo, frameCount, C.MICROS_PER_SECOND); + } + + public SdrImageItemConfig(AssetInfo assetInfo, int frameRate, long durationUs) { + this(assetInfo, frameRate, durationUs, Effects.EMPTY); + } + + public SdrImageItemConfig( + AssetInfo assetInfo, int frameRate, long durationUs, Effects effects) { + super( + assetInfo.uri, + /* frameCount= */ (int) + Util.scaleLargeValue( + frameRate, durationUs, C.MICROS_PER_SECOND, RoundingMode.CEILING), + /* format= */ assetInfo.videoFormat, + /* outputFormat= */ assetInfo + .videoFormat + .buildUpon() + // Image by default are encoded in H265 and BT709 SDR. + .setSampleMimeType(VIDEO_H265) + .setColorInfo(ColorInfo.SDR_BT709_LIMITED) + .setFrameRate(frameRate) + .build(), + effects); + this.frameRate = frameRate; + this.durationUs = durationUs; + } + + @Override + protected void onBuild(EditedMediaItem.Builder builder) { + builder.setFrameRate(frameRate).setDurationUs(durationUs); + } + } + + /** + * {@link ItemConfig} for a video {@link EditedMediaItem}. + * + *

Audio is removed. + */ + public static final class VideoItemConfig extends ItemConfig { + public VideoItemConfig(AssetInfo asset, Effects effects) { + super(asset.uri, asset.videoFrameCount, asset.videoFormat, asset.videoFormat, effects); + } + + @Override + protected void onBuild(EditedMediaItem.Builder builder) { + builder.setRemoveAudio(true); + } + } + + private ParameterizedAndroidTestUtil() {} +} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/ParameterizedInputSequenceExportTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/ParameterizedInputSequenceExportTest.java index fb784626a1..95a16ed8b6 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/ParameterizedInputSequenceExportTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/ParameterizedInputSequenceExportTest.java @@ -14,32 +14,23 @@ * limitations under the License. * */ - package androidx.media3.transformer; import static androidx.media3.transformer.AndroidTestUtil.BT601_MP4_ASSET; import static androidx.media3.transformer.AndroidTestUtil.JPG_ASSET; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET; import static androidx.media3.transformer.AndroidTestUtil.PNG_ASSET; +import static androidx.media3.transformer.ParameterizedAndroidTestUtil.assumeSequenceFormatsSupported; import static com.google.common.truth.Truth.assertThat; import android.content.Context; -import androidx.annotation.Nullable; -import androidx.media3.common.C; -import androidx.media3.common.ColorInfo; -import androidx.media3.common.Format; -import androidx.media3.common.MediaItem; -import androidx.media3.common.MimeTypes; -import androidx.media3.common.util.Assertions; -import androidx.media3.common.util.Util; import androidx.media3.effect.Presentation; -import androidx.media3.transformer.AndroidTestUtil.AssetInfo; +import androidx.media3.transformer.ParameterizedAndroidTestUtil.SdrImageItemConfig; +import androidx.media3.transformer.ParameterizedAndroidTestUtil.SequenceConfig; +import androidx.media3.transformer.ParameterizedAndroidTestUtil.VideoItemConfig; import androidx.test.core.app.ApplicationProvider; -import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; import java.io.File; -import java.math.RoundingMode; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; @@ -57,12 +48,22 @@ import org.junit.runners.Parameterized.Parameters; /** Parameterized end to end {@linkplain EditedMediaItemSequence sequence} export tests. */ @RunWith(Parameterized.class) public class ParameterizedInputSequenceExportTest { - private static final ImageItemConfig PNG_ITEM = - new ImageItemConfig(PNG_ASSET, /* frameCount= */ 34); - private static final ImageItemConfig JPG_ITEM = - new ImageItemConfig(JPG_ASSET, /* frameCount= */ 41); - private static final VideoItemConfig BT709_ITEM = new VideoItemConfig(MP4_ASSET); - private static final VideoItemConfig BT601_ITEM = new VideoItemConfig(BT601_MP4_ASSET); + private static final SdrImageItemConfig PNG_ITEM = + new SdrImageItemConfig(PNG_ASSET, /* frameCount= */ 34); + private static final SdrImageItemConfig JPG_ITEM = + new SdrImageItemConfig(JPG_ASSET, /* frameCount= */ 41); + private static final VideoItemConfig BT709_ITEM = + new VideoItemConfig( + MP4_ASSET, + new Effects( + /* audioProcessors= */ ImmutableList.of(), + ImmutableList.of(Presentation.createForHeight(360)))); + private static final VideoItemConfig BT601_ITEM = + new VideoItemConfig( + BT601_MP4_ASSET, + new Effects( + /* audioProcessors= */ ImmutableList.of(), + ImmutableList.of(Presentation.createForHeight(360)))); @Parameters(name = "{0}") public static ImmutableList params() { @@ -117,161 +118,19 @@ public class ParameterizedInputSequenceExportTest { ExportTestResult result = new TransformerAndroidTestRunner.Builder(context, transformer) .build() - .run(testId, sequence.buildComposition()); + .run( + testId, + sequence.buildComposition( + // Presentation of 480Wx360H is used to ensure software encoders can encode. + new Effects( + /* audioProcessors= */ ImmutableList.of(), + ImmutableList.of( + Presentation.createForWidthAndHeight( + /* width= */ 480, + /* height= */ 360, + Presentation.LAYOUT_SCALE_TO_FIT))))); assertThat(result.exportResult.videoFrameCount).isEqualTo(sequence.totalExpectedFrameCount); assertThat(new File(result.filePath).length()).isGreaterThan(0); } - - private static void assumeSequenceFormatsSupported( - Context context, String testId, SequenceConfig sequence) throws Exception { - Assertions.checkState(!sequence.itemConfigs.isEmpty()); - Format outputFormat = Assertions.checkNotNull(sequence.itemConfigs.get(0).outputFormat); - for (ItemConfig item : sequence.itemConfigs) { - AndroidTestUtil.assumeFormatsSupported(context, testId, item.format, outputFormat); - } - } - - /** Test parameters for an {@link EditedMediaItemSequence}. */ - private static final class SequenceConfig { - public final int totalExpectedFrameCount; - public final ImmutableList itemConfigs; - - public SequenceConfig(ItemConfig... itemConfigs) { - this.itemConfigs = ImmutableList.copyOf(itemConfigs); - int frameCountSum = 0; - for (ItemConfig item : itemConfigs) { - frameCountSum += item.frameCount; - } - this.totalExpectedFrameCount = frameCountSum; - } - - /** - * Builds a {@link Composition} from the sequence configuration. - * - *

{@link Presentation} of {@code width 480, height 360} is used to ensure software encoders - * can encode. - */ - public Composition buildComposition() { - ImmutableList.Builder editedMediaItems = new ImmutableList.Builder<>(); - for (ItemConfig itemConfig : itemConfigs) { - editedMediaItems.add(itemConfig.build()); - } - - return new Composition.Builder(new EditedMediaItemSequence(editedMediaItems.build())) - .setEffects( - new Effects( - /* audioProcessors= */ ImmutableList.of(), - ImmutableList.of( - Presentation.createForWidthAndHeight( - /* width= */ 480, /* height= */ 360, Presentation.LAYOUT_SCALE_TO_FIT)))) - .build(); - } - - @Override - public String toString() { - StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append("Seq{"); - for (ItemConfig itemConfig : itemConfigs) { - stringBuilder.append(itemConfig).append(","); - } - stringBuilder.replace(stringBuilder.length() - 2, stringBuilder.length(), "}"); - return stringBuilder.toString(); - } - } - - /** Test parameters for an {@link EditedMediaItem}. */ - private abstract static class ItemConfig { - public final int frameCount; - - private final String uri; - @Nullable private final Format format; - @Nullable private final Format outputFormat; - - public ItemConfig( - String uri, int frameCount, @Nullable Format format, @Nullable Format outputFormat) { - this.uri = uri; - this.frameCount = frameCount; - this.format = format; - this.outputFormat = outputFormat; - } - - public final EditedMediaItem build() { - EditedMediaItem.Builder builder = new EditedMediaItem.Builder(MediaItem.fromUri(uri)); - onBuild(builder); - return builder.build(); - } - - /** - * Called when an {@link EditedMediaItem} is being {@linkplain #build() built}. - * - * @param builder The {@link EditedMediaItem.Builder} to optionally modify before the item is - * built. - */ - protected abstract void onBuild(EditedMediaItem.Builder builder); - - @Override - public String toString() { - return Iterables.getLast(Splitter.on("/").splitToList(uri)); - } - } - - /** {@link ItemConfig} for an image {@link EditedMediaItem} with a duration of one second. */ - private static final class ImageItemConfig extends ItemConfig { - - // Image by default are encoded in H265 and BT709 SDR. - private static final Format OUTPUT_FORMAT = - new Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H265) - .setFrameRate(30.f) - .setWidth(480) - .setHeight(360) - .setColorInfo(ColorInfo.SDR_BT709_LIMITED) - .build(); - - private final long durationUs; - private final int frameRate; - - public ImageItemConfig(AssetInfo assetInfo, int frameCount) { - this(assetInfo, frameCount, C.MICROS_PER_SECOND); - } - - public ImageItemConfig(AssetInfo assetInfo, int frameRate, long durationUs) { - super( - assetInfo.uri, - /* frameCount= */ (int) - Util.scaleLargeValue( - frameRate, durationUs, C.MICROS_PER_SECOND, RoundingMode.CEILING), - /* format= */ assetInfo.videoFormat, - OUTPUT_FORMAT); - this.frameRate = frameRate; - this.durationUs = durationUs; - } - - @Override - protected void onBuild(EditedMediaItem.Builder builder) { - builder.setFrameRate(frameRate).setDurationUs(durationUs); - } - } - - /** - * {@link ItemConfig} for a video {@link EditedMediaItem}. - * - *

Audio is removed and a {@link Presentation} of specified {@code height=360}. - */ - private static final class VideoItemConfig extends ItemConfig { - public VideoItemConfig(AssetInfo asset) { - super(asset.uri, asset.videoFrameCount, asset.videoFormat, asset.videoFormat); - } - - @Override - protected void onBuild(EditedMediaItem.Builder builder) { - builder - .setEffects( - new Effects( - /* audioProcessors= */ ImmutableList.of(), - ImmutableList.of(Presentation.createForHeight(360)))) - .setRemoveAudio(true); - } - } }