Implement video effects for Frame Extraction

Test that downscaled images match MediaMetadataRetriever.

PiperOrigin-RevId: 696862566
This commit is contained in:
dancho 2024-11-15 06:06:59 -08:00 committed by Copybara-Service
parent e6448f3498
commit 11fc0871ac
3 changed files with 52 additions and 15 deletions

View File

@ -31,6 +31,7 @@ import android.graphics.Bitmap;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.NullableType; import androidx.media3.common.util.NullableType;
import androidx.media3.effect.Presentation;
import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.transformer.ExperimentalFrameExtractor.Frame; import androidx.media3.transformer.ExperimentalFrameExtractor.Frame;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
@ -91,7 +92,8 @@ public class FrameExtractorTest {
new ExperimentalFrameExtractor( new ExperimentalFrameExtractor(
context, context,
new ExperimentalFrameExtractor.Configuration.Builder().build(), new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH)); MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 8_500); ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 8_500);
Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS); Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
@ -106,13 +108,36 @@ public class FrameExtractorTest {
assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD); 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<Frame> 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 @Test
public void extractFrame_pastDuration_returnsLastFrame() throws Exception { public void extractFrame_pastDuration_returnsLastFrame() throws Exception {
frameExtractor = frameExtractor =
new ExperimentalFrameExtractor( new ExperimentalFrameExtractor(
context, context,
new ExperimentalFrameExtractor.Configuration.Builder().build(), new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH)); MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 200_000); ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 200_000);
Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS); Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
@ -134,7 +159,8 @@ public class FrameExtractorTest {
new ExperimentalFrameExtractor( new ExperimentalFrameExtractor(
context, context,
new ExperimentalFrameExtractor.Configuration.Builder().build(), new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH)); MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
ImmutableList<Long> requestedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 34L, 34L); ImmutableList<Long> requestedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 34L, 34L);
ImmutableList<Long> expectedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 66L, 66L); ImmutableList<Long> expectedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 66L, 66L);
List<ListenableFuture<Frame>> frameFutures = new ArrayList<>(); List<ListenableFuture<Frame>> frameFutures = new ArrayList<>();
@ -164,7 +190,8 @@ public class FrameExtractorTest {
new ExperimentalFrameExtractor( new ExperimentalFrameExtractor(
context, context,
new ExperimentalFrameExtractor.Configuration.Builder().build(), new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH)); MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
ListenableFuture<Frame> frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000); ListenableFuture<Frame> frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000);
ListenableFuture<Frame> frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000); ListenableFuture<Frame> frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000);
@ -187,7 +214,8 @@ public class FrameExtractorTest {
new ExperimentalFrameExtractor.Configuration.Builder() new ExperimentalFrameExtractor.Configuration.Builder()
.setSeekParameters(CLOSEST_SYNC) .setSeekParameters(CLOSEST_SYNC)
.build(), .build(),
MediaItem.fromUri(FILE_PATH)); MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
ListenableFuture<Frame> frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000); ListenableFuture<Frame> frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000);
ListenableFuture<Frame> frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000); ListenableFuture<Frame> frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000);
@ -211,7 +239,8 @@ public class FrameExtractorTest {
new ExperimentalFrameExtractor( new ExperimentalFrameExtractor(
context, context,
new ExperimentalFrameExtractor.Configuration.Builder().build(), new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(filePath)); MediaItem.fromUri(filePath),
/* effects= */ ImmutableList.of());
ListenableFuture<Frame> frame0 = frameExtractor.getFrame(/* positionMs= */ 0); ListenableFuture<Frame> frame0 = frameExtractor.getFrame(/* positionMs= */ 0);
@ -228,7 +257,8 @@ public class FrameExtractorTest {
new ExperimentalFrameExtractor( new ExperimentalFrameExtractor(
context, context,
new ExperimentalFrameExtractor.Configuration.Builder().build(), new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH)); MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
AtomicReference<@NullableType Frame> frameAtomicReference = new AtomicReference<>(); AtomicReference<@NullableType Frame> frameAtomicReference = new AtomicReference<>();
AtomicReference<@NullableType Throwable> throwableAtomicReference = new AtomicReference<>(); AtomicReference<@NullableType Throwable> throwableAtomicReference = new AtomicReference<>();
ConditionVariable frameReady = new ConditionVariable(); ConditionVariable frameReady = new ConditionVariable();
@ -262,7 +292,8 @@ public class FrameExtractorTest {
new ExperimentalFrameExtractor( new ExperimentalFrameExtractor(
context, context,
new ExperimentalFrameExtractor.Configuration.Builder().build(), new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH)); MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
instrumentation.runOnMainSync(frameExtractor::release); instrumentation.runOnMainSync(frameExtractor::release);

View File

@ -53,6 +53,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import org.checkerframework.checker.initialization.qual.Initialized; import org.checkerframework.checker.initialization.qual.Initialized;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -148,12 +149,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* Creates an instance. * Creates an instance.
* *
* @param context {@link Context}. * @param context {@link Context}.
* @param configuration The {@link Configuration} for this frame extractor.
* @param mediaItem The {@link MediaItem} from which frames are extracted. * @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 changing the MediaItem.
// TODO: b/350498258 - Support video effects.
public ExperimentalFrameExtractor( public ExperimentalFrameExtractor(
Context context, Configuration configuration, MediaItem mediaItem) { Context context, Configuration configuration, MediaItem mediaItem, List<Effect> effects) {
player = new ExoPlayer.Builder(context).setSeekParameters(configuration.seekParameters).build(); player = new ExoPlayer.Builder(context).setSeekParameters(configuration.seekParameters).build();
playerApplicationThreadHandler = new Handler(player.getApplicationLooper()); playerApplicationThreadHandler = new Handler(player.getApplicationLooper());
lastRequestedFrameFuture = SettableFuture.create(); lastRequestedFrameFuture = SettableFuture.create();
@ -168,7 +171,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
playerApplicationThreadHandler.post( playerApplicationThreadHandler.post(
() -> { () -> {
player.addAnalyticsListener(thisRef); player.addAnalyticsListener(thisRef);
player.setVideoEffects(buildVideoEffects()); player.setVideoEffects(buildVideoEffects(effects));
player.setMediaItem(mediaItem); player.setMediaItem(mediaItem);
player.setPlayWhenReady(false); player.setPlayWhenReady(false);
player.prepare(); player.prepare();
@ -272,15 +275,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
private ImmutableList<Effect> buildVideoEffects() { private ImmutableList<Effect> buildVideoEffects(List<Effect> effects) {
return ImmutableList.of( ImmutableList.Builder<Effect> listBuilder = new ImmutableList.Builder<>();
listBuilder.addAll(effects);
listBuilder.add(
(MatrixTransformation) (MatrixTransformation)
presentationTimeUs -> { presentationTimeUs -> {
Matrix mirrorY = new Matrix(); Matrix mirrorY = new Matrix();
mirrorY.setScale(/* sx= */ 1, /* sy= */ -1); mirrorY.setScale(/* sx= */ 1, /* sy= */ -1);
return mirrorY; return mirrorY;
}, });
new FrameReader()); listBuilder.add(new FrameReader());
return listBuilder.build();
} }
private final class FrameReader implements GlEffect { private final class FrameReader implements GlEffect {