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 {