mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Implement video effects for Frame Extraction
Test that downscaled images match MediaMetadataRetriever. PiperOrigin-RevId: 696862566
This commit is contained in:
parent
e6448f3498
commit
11fc0871ac
Binary file not shown.
After Width: | Height: | Size: 106 KiB |
@ -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);
|
||||||
|
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user