diff --git a/libraries/test_data/src/test/assets/test-generated-goldens/FrameExtractorTest/sample_with_increasing_timestamps_360p_8.531_scaled_to_180p.png b/libraries/test_data/src/test/assets/test-generated-goldens/FrameExtractorTest/sample_with_increasing_timestamps_360p_8.531_scaled_to_180p.png new file mode 100644 index 0000000000..d63a98da58 Binary files /dev/null and b/libraries/test_data/src/test/assets/test-generated-goldens/FrameExtractorTest/sample_with_increasing_timestamps_360p_8.531_scaled_to_180p.png differ diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameExtractorTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameExtractorTest.java index 5c69a91c2f..0127262b73 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameExtractorTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameExtractorTest.java @@ -31,6 +31,7 @@ import android.graphics.Bitmap; import androidx.media3.common.MediaItem; import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.NullableType; +import androidx.media3.effect.Presentation; import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.transformer.ExperimentalFrameExtractor.Frame; import androidx.test.core.app.ApplicationProvider; @@ -91,7 +92,8 @@ public class FrameExtractorTest { new ExperimentalFrameExtractor( context, new ExperimentalFrameExtractor.Configuration.Builder().build(), - MediaItem.fromUri(FILE_PATH)); + MediaItem.fromUri(FILE_PATH), + /* effects= */ ImmutableList.of()); ListenableFuture frameFuture = frameExtractor.getFrame(/* positionMs= */ 8_500); Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS); @@ -106,13 +108,36 @@ public class FrameExtractorTest { assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD); } + @Test + public void extractFrame_oneFrameWithPresentationEffect_returnsScaledFrame() throws Exception { + frameExtractor = + new ExperimentalFrameExtractor( + context, + new ExperimentalFrameExtractor.Configuration.Builder().build(), + MediaItem.fromUri(FILE_PATH), + /* effects= */ ImmutableList.of(Presentation.createForHeight(180))); + + ListenableFuture frameFuture = frameExtractor.getFrame(/* positionMs= */ 8_500); + Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS); + Bitmap actualBitmap = frame.bitmap; + Bitmap expectedBitmap = + readBitmap( + /* assetString= */ GOLDEN_ASSET_FOLDER_PATH + + "sample_with_increasing_timestamps_360p_8.531_scaled_to_180p.png"); + maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null); + + assertThat(frame.presentationTimeMs).isEqualTo(8_531); + assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD); + } + @Test public void extractFrame_pastDuration_returnsLastFrame() throws Exception { frameExtractor = new ExperimentalFrameExtractor( context, new ExperimentalFrameExtractor.Configuration.Builder().build(), - MediaItem.fromUri(FILE_PATH)); + MediaItem.fromUri(FILE_PATH), + /* effects= */ ImmutableList.of()); ListenableFuture frameFuture = frameExtractor.getFrame(/* positionMs= */ 200_000); Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS); @@ -134,7 +159,8 @@ public class FrameExtractorTest { new ExperimentalFrameExtractor( context, new ExperimentalFrameExtractor.Configuration.Builder().build(), - MediaItem.fromUri(FILE_PATH)); + MediaItem.fromUri(FILE_PATH), + /* effects= */ ImmutableList.of()); ImmutableList requestedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 34L, 34L); ImmutableList expectedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 66L, 66L); List> frameFutures = new ArrayList<>(); @@ -164,7 +190,8 @@ public class FrameExtractorTest { new ExperimentalFrameExtractor( context, new ExperimentalFrameExtractor.Configuration.Builder().build(), - MediaItem.fromUri(FILE_PATH)); + MediaItem.fromUri(FILE_PATH), + /* effects= */ ImmutableList.of()); ListenableFuture frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000); ListenableFuture frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000); @@ -187,7 +214,8 @@ public class FrameExtractorTest { new ExperimentalFrameExtractor.Configuration.Builder() .setSeekParameters(CLOSEST_SYNC) .build(), - MediaItem.fromUri(FILE_PATH)); + MediaItem.fromUri(FILE_PATH), + /* effects= */ ImmutableList.of()); ListenableFuture frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000); ListenableFuture frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000); @@ -211,7 +239,8 @@ public class FrameExtractorTest { new ExperimentalFrameExtractor( context, new ExperimentalFrameExtractor.Configuration.Builder().build(), - MediaItem.fromUri(filePath)); + MediaItem.fromUri(filePath), + /* effects= */ ImmutableList.of()); ListenableFuture frame0 = frameExtractor.getFrame(/* positionMs= */ 0); @@ -228,7 +257,8 @@ public class FrameExtractorTest { new ExperimentalFrameExtractor( context, new ExperimentalFrameExtractor.Configuration.Builder().build(), - MediaItem.fromUri(FILE_PATH)); + MediaItem.fromUri(FILE_PATH), + /* effects= */ ImmutableList.of()); AtomicReference<@NullableType Frame> frameAtomicReference = new AtomicReference<>(); AtomicReference<@NullableType Throwable> throwableAtomicReference = new AtomicReference<>(); ConditionVariable frameReady = new ConditionVariable(); @@ -262,7 +292,8 @@ public class FrameExtractorTest { new ExperimentalFrameExtractor( context, new ExperimentalFrameExtractor.Configuration.Builder().build(), - MediaItem.fromUri(FILE_PATH)); + MediaItem.fromUri(FILE_PATH), + /* effects= */ ImmutableList.of()); Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); instrumentation.runOnMainSync(frameExtractor::release); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ExperimentalFrameExtractor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ExperimentalFrameExtractor.java index ca5aae1695..8a7e938956 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ExperimentalFrameExtractor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ExperimentalFrameExtractor.java @@ -53,6 +53,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.nio.ByteBuffer; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; import org.checkerframework.checker.initialization.qual.Initialized; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -148,12 +149,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * Creates an instance. * * @param context {@link Context}. + * @param configuration The {@link Configuration} for this frame extractor. * @param mediaItem The {@link MediaItem} from which frames are extracted. + * @param effects The {@link List} of {@linkplain Effect video effects} to apply to the extracted + * video frames. */ // TODO: b/350498258 - Support changing the MediaItem. - // TODO: b/350498258 - Support video effects. public ExperimentalFrameExtractor( - Context context, Configuration configuration, MediaItem mediaItem) { + Context context, Configuration configuration, MediaItem mediaItem, List effects) { player = new ExoPlayer.Builder(context).setSeekParameters(configuration.seekParameters).build(); playerApplicationThreadHandler = new Handler(player.getApplicationLooper()); lastRequestedFrameFuture = SettableFuture.create(); @@ -168,7 +171,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; playerApplicationThreadHandler.post( () -> { player.addAnalyticsListener(thisRef); - player.setVideoEffects(buildVideoEffects()); + player.setVideoEffects(buildVideoEffects(effects)); player.setMediaItem(mediaItem); player.setPlayWhenReady(false); player.prepare(); @@ -272,15 +275,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } - private ImmutableList buildVideoEffects() { - return ImmutableList.of( + private ImmutableList buildVideoEffects(List effects) { + ImmutableList.Builder listBuilder = new ImmutableList.Builder<>(); + listBuilder.addAll(effects); + listBuilder.add( (MatrixTransformation) presentationTimeUs -> { Matrix mirrorY = new Matrix(); mirrorY.setScale(/* sx= */ 1, /* sy= */ -1); return mirrorY; - }, - new FrameReader()); + }); + listBuilder.add(new FrameReader()); + return listBuilder.build(); } private final class FrameReader implements GlEffect {