diff --git a/libraries/test_data/src/test/assets/media/bitmap/transformer_multi_sequence_composition_test/export_withTwoSequencesEachWithOneVideoMediaItem_succeeds_0.png b/libraries/test_data/src/test/assets/media/bitmap/transformer_multi_sequence_composition_test/export_withTwoSequencesEachWithOneVideoMediaItem_succeeds_0.png new file mode 100644 index 0000000000..0e9e3566ac Binary files /dev/null and b/libraries/test_data/src/test/assets/media/bitmap/transformer_multi_sequence_composition_test/export_withTwoSequencesEachWithOneVideoMediaItem_succeeds_0.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/transformer_multi_sequence_composition_test/export_withTwoSequencesOneWithVideoOneWithImage_succeeds_0.png b/libraries/test_data/src/test/assets/media/bitmap/transformer_multi_sequence_composition_test/export_withTwoSequencesOneWithVideoOneWithImage_succeeds_0.png new file mode 100644 index 0000000000..480cdac13a Binary files /dev/null and b/libraries/test_data/src/test/assets/media/bitmap/transformer_multi_sequence_composition_test/export_withTwoSequencesOneWithVideoOneWithImage_succeeds_0.png differ diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/BitmapPixelTestUtil.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/BitmapPixelTestUtil.java index 80c58b0d2a..7071cb7505 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/BitmapPixelTestUtil.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/BitmapPixelTestUtil.java @@ -118,6 +118,13 @@ public class BitmapPixelTestUtil { */ public static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_DIFFERENT_DEVICE_FP16 = .01f; + /** + * Maximum allowed average pixel difference between bitmaps generated from luma values. + * + * @see #MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE + */ + public static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_LUMA = 8.0f; + /** * Reads a bitmap from the specified asset location. * diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java index cbf50d7374..6868901439 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java @@ -22,9 +22,11 @@ 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.common.util.Util.SDK_INT; +import static org.junit.Assume.assumeFalse; import android.content.Context; import android.graphics.Bitmap; +import android.media.Image; import android.media.MediaFormat; import android.opengl.EGLContext; import android.opengl.EGLDisplay; @@ -46,6 +48,9 @@ import androidx.media3.common.util.Util; import androidx.media3.effect.DefaultGlObjectsProvider; import androidx.media3.effect.ScaleAndRotateTransformation; import androidx.media3.exoplayer.mediacodec.MediaCodecUtil; +import androidx.media3.test.utils.BitmapPixelTestUtil; +import androidx.media3.test.utils.VideoDecodingWrapper; +import com.google.common.base.Ascii; import com.google.common.collect.ImmutableList; import java.io.File; import java.io.FileWriter; @@ -600,6 +605,27 @@ public final class AndroidTestUtil { writeTestSummaryToFile(context, testId, testJson); } + public static ImmutableList extractBitmapsFromVideo(Context context, String filePath) + throws IOException, InterruptedException { + // b/298599172 - runUntilComparisonFrameOrEnded fails on this device because reading decoder + // output as a bitmap doesn't work. + assumeFalse(Util.SDK_INT == 21 && Ascii.toLowerCase(Util.MODEL).contains("nexus")); + ImmutableList.Builder bitmaps = new ImmutableList.Builder<>(); + try (VideoDecodingWrapper decodingWrapper = + new VideoDecodingWrapper( + context, filePath, /* comparisonInterval= */ 1, /* maxImagesAllowed= */ 1)) { + while (true) { + @Nullable Image image = decodingWrapper.runUntilComparisonFrameOrEnded(); + if (image == null) { + break; + } + bitmaps.add(BitmapPixelTestUtil.createGrayscaleArgb8888BitmapFromYuv420888Image(image)); + image.close(); + } + } + return bitmaps.build(); + } + /** A customizable forwarding {@link Codec.EncoderFactory} that forces encoding. */ public static final class ForceEncodeEncoderFactory implements Codec.EncoderFactory { diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerMultiSequenceCompositionTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerMultiSequenceCompositionTest.java new file mode 100644 index 0000000000..956ee7421d --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerMultiSequenceCompositionTest.java @@ -0,0 +1,196 @@ +/* + * 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 + * + * https://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.Assertions.checkNotNull; +import static androidx.media3.common.util.Util.msToUs; +import static androidx.media3.test.utils.BitmapPixelTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_LUMA; +import static androidx.media3.test.utils.BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888; +import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap; +import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap; +import static androidx.media3.transformer.AndroidTestUtil.JPG_ASSET_URI_STRING; +import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_FORMAT; +import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING; +import static androidx.media3.transformer.AndroidTestUtil.extractBitmapsFromVideo; +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.graphics.Bitmap; +import androidx.media3.common.Effect; +import androidx.media3.common.MediaItem; +import androidx.media3.common.util.Util; +import androidx.media3.effect.AlphaScale; +import androidx.media3.effect.Contrast; +import androidx.media3.effect.Presentation; +import androidx.media3.effect.ScaleAndRotateTransformation; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for using multiple {@link EditedMediaItemSequence} in a composition. */ +@RunWith(AndroidJUnit4.class) +public final class TransformerMultiSequenceCompositionTest { + + private static final String PNG_ASSET_BASE_PATH = + "media/bitmap/transformer_multi_sequence_composition_test"; + + // The duration of one frame of the 30 FPS test video, in milliseconds. + private static final long ONE_FRAME_DURATION_MS = 35; + private static final int EXPORT_WIDTH = 360; + private static final int EXPORT_HEIGHT = 240; + + private final Context context = ApplicationProvider.getApplicationContext(); + + @Test + public void export_withTwoSequencesEachWithOneVideoMediaItem_succeeds() throws Exception { + String testId = "export_withTwoSequencesEachWithOneVideoMediaItem_succeeds"; + if (AndroidTestUtil.skipAndLogIfFormatsUnsupported( + context, + testId, + /* inputFormat= */ MP4_ASSET_FORMAT, + /* outputFormat= */ MP4_ASSET_FORMAT)) { + return; + } + + Composition composition = + createComposition( + /* compositionEffects= */ ImmutableList.of( + new Contrast(0.1f), + Presentation.createForWidthAndHeight( + EXPORT_WIDTH, EXPORT_HEIGHT, Presentation.LAYOUT_SCALE_TO_FIT)), + /* firstSequenceMediaItems= */ ImmutableList.of( + editedMediaItemByClippingVideo( + MP4_ASSET_URI_STRING, + /* effects= */ ImmutableList.of( + new AlphaScale(0.5f), + new ScaleAndRotateTransformation.Builder() + .setRotationDegrees(180) + .build()))), + /* secondSequenceMediaItems= */ ImmutableList.of( + editedMediaItemByClippingVideo( + MP4_ASSET_URI_STRING, /* effects= */ ImmutableList.of()))); + + ExportTestResult result = + new TransformerAndroidTestRunner.Builder(context, new Transformer.Builder(context).build()) + .build() + .run(testId, composition); + + assertThat(result.filePath).isNotNull(); + assertBitmapsMatchExpected( + extractBitmapsFromVideo(context, checkNotNull(result.filePath)), testId); + } + + @Test + public void export_withTwoSequencesOneWithVideoOneWithImage_succeeds() throws Exception { + String testId = "export_withTwoSequencesOneWithVideoOneWithImage_succeeds"; + if (AndroidTestUtil.skipAndLogIfFormatsUnsupported( + context, + testId, + /* inputFormat= */ MP4_ASSET_FORMAT, + /* outputFormat= */ MP4_ASSET_FORMAT)) { + return; + } + + Composition composition = + createComposition( + /* compositionEffects= */ ImmutableList.of( + new Contrast(0.1f), + Presentation.createForWidthAndHeight( + EXPORT_WIDTH, EXPORT_HEIGHT, Presentation.LAYOUT_SCALE_TO_FIT)), + /* firstSequenceMediaItems= */ ImmutableList.of( + editedMediaItemByClippingVideo( + MP4_ASSET_URI_STRING, + /* effects= */ ImmutableList.of( + new AlphaScale(0.5f), + new ScaleAndRotateTransformation.Builder() + .setRotationDegrees(180) + .build()))), + /* secondSequenceMediaItems= */ ImmutableList.of( + editedMediaItemOfOneFrameImage( + JPG_ASSET_URI_STRING, /* effects= */ ImmutableList.of()))); + + ExportTestResult result = + new TransformerAndroidTestRunner.Builder(context, new Transformer.Builder(context).build()) + .build() + .run(testId, composition); + + assertThat(result.filePath).isNotNull(); + assertBitmapsMatchExpected( + extractBitmapsFromVideo(context, checkNotNull(result.filePath)), testId); + } + + private static EditedMediaItem editedMediaItemByClippingVideo(String uri, List effects) { + return new EditedMediaItem.Builder( + MediaItem.fromUri(uri) + .buildUpon() + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder() + .setEndPositionMs(ONE_FRAME_DURATION_MS) + .build()) + .build()) + .setRemoveAudio(true) + .setEffects( + new Effects(/* audioProcessors= */ ImmutableList.of(), ImmutableList.copyOf(effects))) + .build(); + } + + private static EditedMediaItem editedMediaItemOfOneFrameImage(String uri, List effects) { + return new EditedMediaItem.Builder(MediaItem.fromUri(uri)) + .setRemoveAudio(true) + .setDurationUs(msToUs(ONE_FRAME_DURATION_MS)) + .setFrameRate((int) (1000 / ONE_FRAME_DURATION_MS)) + .setEffects( + new Effects(/* audioProcessors= */ ImmutableList.of(), ImmutableList.copyOf(effects))) + .build(); + } + + private static Composition createComposition( + List compositionEffects, + List firstSequenceMediaItems, + List secondSequenceMediaItems) { + + return new Composition.Builder( + ImmutableList.of( + new EditedMediaItemSequence(firstSequenceMediaItems), + new EditedMediaItemSequence(secondSequenceMediaItems))) + .setEffects( + new Effects( + /* audioProcessors= */ ImmutableList.of(), /* videoEffects= */ compositionEffects)) + .build(); + } + + private static void assertBitmapsMatchExpected(List actualBitmaps, String testId) + throws IOException { + for (int i = 0; i < actualBitmaps.size(); i++) { + Bitmap actualBitmap = actualBitmaps.get(i); + String subTestId = testId + "_" + i; + Bitmap expectedBitmap = + readBitmap(Util.formatInvariant("%s/%s.png", PNG_ASSET_BASE_PATH, subTestId)); + + maybeSaveTestBitmap( + testId, /* bitmapLabel= */ String.valueOf(i), actualBitmap, /* path= */ null); + float averagePixelAbsoluteDifference = + getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, subTestId); + assertThat(averagePixelAbsoluteDifference) + .isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_LUMA); + } + } +} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerSequenceEffectTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerSequenceEffectTest.java index 1b5280be62..ee09cf6df7 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerSequenceEffectTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerSequenceEffectTest.java @@ -18,6 +18,7 @@ package androidx.media3.transformer; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.test.utils.BitmapPixelTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_LUMA; import static androidx.media3.test.utils.BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888; import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap; import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap; @@ -26,12 +27,11 @@ import static androidx.media3.transformer.AndroidTestUtil.JPG_PORTRAIT_ASSET_URI import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_FORMAT; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING; import static androidx.media3.transformer.AndroidTestUtil.MP4_PORTRAIT_ASSET_URI_STRING; +import static androidx.media3.transformer.AndroidTestUtil.extractBitmapsFromVideo; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assume.assumeFalse; import android.content.Context; import android.graphics.Bitmap; -import android.media.Image; import androidx.annotation.Nullable; import androidx.media3.common.Effect; import androidx.media3.common.MediaItem; @@ -41,11 +41,8 @@ import androidx.media3.effect.OverlayEffect; import androidx.media3.effect.Presentation; import androidx.media3.effect.RgbFilter; import androidx.media3.effect.ScaleAndRotateTransformation; -import androidx.media3.test.utils.BitmapPixelTestUtil; -import androidx.media3.test.utils.VideoDecodingWrapper; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.common.base.Ascii; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.util.List; @@ -65,13 +62,6 @@ public final class TransformerSequenceEffectTest { private static final int EXPORT_WIDTH = 360; private static final int EXPORT_HEIGHT = 240; - /** - * Maximum allowed average pixel difference between bitmaps generated from luma values. - * - * @see BitmapPixelTestUtil#MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE - */ - private static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_LUMA = 8.0f; - private final Context context = ApplicationProvider.getApplicationContext(); @Test @@ -257,28 +247,7 @@ public final class TransformerSequenceEffectTest { .build(); } - private static ImmutableList extractBitmapsFromVideo(Context context, String filePath) - throws IOException, InterruptedException { - // b/298599172 - runUntilComparisonFrameOrEnded fails on this device because reading decoder - // output as a bitmap doesn't work. - assumeFalse(Util.SDK_INT == 21 && Ascii.toLowerCase(Util.MODEL).contains("nexus")); - ImmutableList.Builder bitmaps = new ImmutableList.Builder<>(); - try (VideoDecodingWrapper decodingWrapper = - new VideoDecodingWrapper( - context, filePath, /* comparisonInterval= */ 1, /* maxImagesAllowed= */ 1)) { - while (true) { - @Nullable Image image = decodingWrapper.runUntilComparisonFrameOrEnded(); - if (image == null) { - break; - } - bitmaps.add(BitmapPixelTestUtil.createGrayscaleArgb8888BitmapFromYuv420888Image(image)); - image.close(); - } - } - return bitmaps.build(); - } - - private static void assertBitmapsMatchExpected(List actualBitmaps, String testId) + static void assertBitmapsMatchExpected(List actualBitmaps, String testId) throws IOException { for (int i = 0; i < actualBitmaps.size(); i++) { Bitmap actualBitmap = actualBitmaps.get(i);