mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add tests for multi input video
PiperOrigin-RevId: 716208222
This commit is contained in:
parent
b2c31b0743
commit
1732892927
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 417 KiB |
Binary file not shown.
After Width: | Height: | Size: 511 KiB |
@ -0,0 +1,216 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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
|
||||||
|
*
|
||||||
|
* http://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.transformer.AndroidTestUtil.MP4_ASSET;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.PNG_ASSET;
|
||||||
|
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.PlaybackException;
|
||||||
|
import androidx.media3.effect.GlEffect;
|
||||||
|
import androidx.media3.effect.MultipleInputVideoGraph;
|
||||||
|
import androidx.media3.effect.PreviewingMultipleInputVideoGraph;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Playback test of {@link CompositionPlayer} using {@link MultipleInputVideoGraph}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class CompositionMultipleSequencePlaybackTest {
|
||||||
|
private static final long TEST_TIMEOUT_MS = 20_000;
|
||||||
|
private static final MediaItem VIDEO_MEDIA_ITEM = MediaItem.fromUri(MP4_ASSET.uri);
|
||||||
|
private static final long VIDEO_DURATION_US = MP4_ASSET.videoDurationUs;
|
||||||
|
private static final EditedMediaItem VIDEO_EDITED_MEDIA_ITEM =
|
||||||
|
new EditedMediaItem.Builder(VIDEO_MEDIA_ITEM).setDurationUs(VIDEO_DURATION_US).build();
|
||||||
|
private static final ImmutableList<Long> VIDEO_TIMESTAMPS_US = MP4_ASSET.videoTimestampsUs;
|
||||||
|
|
||||||
|
private static final MediaItem IMAGE_MEDIA_ITEM =
|
||||||
|
new MediaItem.Builder().setUri(PNG_ASSET.uri).setImageDurationMs(200).build();
|
||||||
|
private static final long IMAGE_DURATION_US = 200_000;
|
||||||
|
private static final EditedMediaItem IMAGE_EDITED_MEDIA_ITEM =
|
||||||
|
new EditedMediaItem.Builder(IMAGE_MEDIA_ITEM).setDurationUs(IMAGE_DURATION_US).build();
|
||||||
|
// 200 ms at 30 fps (default frame rate)
|
||||||
|
private static final ImmutableList<Long> IMAGE_TIMESTAMPS_US =
|
||||||
|
ImmutableList.of(0L, 33_333L, 66_667L, 100_000L, 133_333L, 166_667L);
|
||||||
|
|
||||||
|
private final Context context = getInstrumentation().getContext().getApplicationContext();
|
||||||
|
private final PlayerTestListener playerTestListener = new PlayerTestListener(TEST_TIMEOUT_MS);
|
||||||
|
|
||||||
|
private @MonotonicNonNull InputTimestampRecordingShaderProgram
|
||||||
|
inputTimestampRecordingShaderProgram;
|
||||||
|
private @MonotonicNonNull CompositionPlayer player;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
inputTimestampRecordingShaderProgram = new InputTimestampRecordingShaderProgram();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() {
|
||||||
|
getInstrumentation()
|
||||||
|
.runOnMainSync(
|
||||||
|
() -> {
|
||||||
|
if (player != null) {
|
||||||
|
player.release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playback_singleSequenceOfVideos_effectsReceiveCorrectTimestamps() throws Exception {
|
||||||
|
Composition composition =
|
||||||
|
new Composition.Builder(
|
||||||
|
new EditedMediaItemSequence.Builder(
|
||||||
|
VIDEO_EDITED_MEDIA_ITEM, VIDEO_EDITED_MEDIA_ITEM)
|
||||||
|
.build())
|
||||||
|
.setEffects(
|
||||||
|
new Effects(
|
||||||
|
/* audioProcessors= */ ImmutableList.of(),
|
||||||
|
/* videoEffects= */ ImmutableList.of(
|
||||||
|
(GlEffect) (context, useHdr) -> inputTimestampRecordingShaderProgram)))
|
||||||
|
.build();
|
||||||
|
ImmutableList<Long> expectedTimestampsUs =
|
||||||
|
new ImmutableList.Builder<Long>()
|
||||||
|
.addAll(VIDEO_TIMESTAMPS_US)
|
||||||
|
.addAll(
|
||||||
|
Iterables.transform(
|
||||||
|
VIDEO_TIMESTAMPS_US, timestampUs -> VIDEO_DURATION_US + timestampUs))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
runCompositionPlayer(composition);
|
||||||
|
|
||||||
|
assertThat(inputTimestampRecordingShaderProgram.getInputTimestampsUs())
|
||||||
|
.isEqualTo(expectedTimestampsUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playback_singleSequenceOfImages_effectsReceiveCorrectTimestamps() throws Exception {
|
||||||
|
Composition composition =
|
||||||
|
new Composition.Builder(
|
||||||
|
new EditedMediaItemSequence.Builder(
|
||||||
|
IMAGE_EDITED_MEDIA_ITEM, IMAGE_EDITED_MEDIA_ITEM)
|
||||||
|
.build())
|
||||||
|
.setEffects(
|
||||||
|
new Effects(
|
||||||
|
/* audioProcessors= */ ImmutableList.of(),
|
||||||
|
/* videoEffects= */ ImmutableList.of(
|
||||||
|
(GlEffect) (context, useHdr) -> inputTimestampRecordingShaderProgram)))
|
||||||
|
.build();
|
||||||
|
ImmutableList<Long> expectedTimestampsUs =
|
||||||
|
new ImmutableList.Builder<Long>()
|
||||||
|
.addAll(IMAGE_TIMESTAMPS_US)
|
||||||
|
.addAll(
|
||||||
|
Iterables.transform(
|
||||||
|
IMAGE_TIMESTAMPS_US, timestampUs -> IMAGE_DURATION_US + timestampUs))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
runCompositionPlayer(composition);
|
||||||
|
|
||||||
|
assertThat(inputTimestampRecordingShaderProgram.getInputTimestampsUs())
|
||||||
|
.isEqualTo(expectedTimestampsUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playback_sequencesOfVideos_effectsReceiveCorrectTimestamps() throws Exception {
|
||||||
|
Composition composition =
|
||||||
|
new Composition.Builder(
|
||||||
|
new EditedMediaItemSequence.Builder(
|
||||||
|
VIDEO_EDITED_MEDIA_ITEM, VIDEO_EDITED_MEDIA_ITEM)
|
||||||
|
.build(),
|
||||||
|
new EditedMediaItemSequence.Builder(
|
||||||
|
VIDEO_EDITED_MEDIA_ITEM, VIDEO_EDITED_MEDIA_ITEM)
|
||||||
|
.build())
|
||||||
|
.setEffects(
|
||||||
|
new Effects(
|
||||||
|
/* audioProcessors= */ ImmutableList.of(),
|
||||||
|
/* videoEffects= */ ImmutableList.of(
|
||||||
|
(GlEffect) (context, useHdr) -> inputTimestampRecordingShaderProgram)))
|
||||||
|
.build();
|
||||||
|
ImmutableList<Long> expectedTimestampsUs =
|
||||||
|
new ImmutableList.Builder<Long>()
|
||||||
|
.addAll(VIDEO_TIMESTAMPS_US)
|
||||||
|
.addAll(
|
||||||
|
Iterables.transform(
|
||||||
|
VIDEO_TIMESTAMPS_US, timestampUs -> VIDEO_DURATION_US + timestampUs))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
runCompositionPlayer(composition);
|
||||||
|
|
||||||
|
assertThat(inputTimestampRecordingShaderProgram.getInputTimestampsUs())
|
||||||
|
.isEqualTo(expectedTimestampsUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playback_sequencesOfImages_effectsReceiveCorrectTimestamps() throws Exception {
|
||||||
|
Composition composition =
|
||||||
|
new Composition.Builder(
|
||||||
|
new EditedMediaItemSequence.Builder(
|
||||||
|
IMAGE_EDITED_MEDIA_ITEM, IMAGE_EDITED_MEDIA_ITEM, IMAGE_EDITED_MEDIA_ITEM)
|
||||||
|
.build(),
|
||||||
|
new EditedMediaItemSequence.Builder(
|
||||||
|
IMAGE_EDITED_MEDIA_ITEM, IMAGE_EDITED_MEDIA_ITEM, IMAGE_EDITED_MEDIA_ITEM)
|
||||||
|
.build())
|
||||||
|
.setEffects(
|
||||||
|
new Effects(
|
||||||
|
/* audioProcessors= */ ImmutableList.of(),
|
||||||
|
/* videoEffects= */ ImmutableList.of(
|
||||||
|
(GlEffect) (context, useHdr) -> inputTimestampRecordingShaderProgram)))
|
||||||
|
.build();
|
||||||
|
ImmutableList<Long> expectedTimestampsUs =
|
||||||
|
new ImmutableList.Builder<Long>()
|
||||||
|
.addAll(IMAGE_TIMESTAMPS_US)
|
||||||
|
.addAll(
|
||||||
|
Iterables.transform(
|
||||||
|
IMAGE_TIMESTAMPS_US, timestampUs -> IMAGE_DURATION_US + timestampUs))
|
||||||
|
.addAll(
|
||||||
|
Iterables.transform(
|
||||||
|
IMAGE_TIMESTAMPS_US, timestampUs -> 2 * IMAGE_DURATION_US + timestampUs))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
runCompositionPlayer(composition);
|
||||||
|
|
||||||
|
assertThat(inputTimestampRecordingShaderProgram.getInputTimestampsUs())
|
||||||
|
.isEqualTo(expectedTimestampsUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runCompositionPlayer(Composition composition)
|
||||||
|
throws PlaybackException, TimeoutException {
|
||||||
|
getInstrumentation()
|
||||||
|
.runOnMainSync(
|
||||||
|
() -> {
|
||||||
|
player =
|
||||||
|
new CompositionPlayer.Builder(context)
|
||||||
|
.setPreviewingVideoGraphFactory(
|
||||||
|
new PreviewingMultipleInputVideoGraph.Factory())
|
||||||
|
.build();
|
||||||
|
player.addListener(playerTestListener);
|
||||||
|
player.setComposition(composition);
|
||||||
|
player.prepare();
|
||||||
|
player.play();
|
||||||
|
});
|
||||||
|
playerTestListener.waitUntilPlayerEnded();
|
||||||
|
}
|
||||||
|
}
|
@ -16,11 +16,13 @@
|
|||||||
|
|
||||||
package androidx.media3.transformer.mh.performance;
|
package androidx.media3.transformer.mh.performance;
|
||||||
|
|
||||||
|
import static androidx.media3.common.util.Util.usToMs;
|
||||||
import static androidx.media3.test.utils.BitmapPixelTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE;
|
import static androidx.media3.test.utils.BitmapPixelTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE;
|
||||||
import static androidx.media3.test.utils.BitmapPixelTestUtil.createArgb8888BitmapFromRgba8888Image;
|
import static androidx.media3.test.utils.BitmapPixelTestUtil.createArgb8888BitmapFromRgba8888Image;
|
||||||
import static androidx.media3.test.utils.BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888;
|
import static androidx.media3.test.utils.BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888;
|
||||||
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
|
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET;
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.PNG_ASSET;
|
||||||
import static androidx.media3.transformer.mh.performance.PlaybackTestUtil.createTimestampOverlay;
|
import static androidx.media3.transformer.mh.performance.PlaybackTestUtil.createTimestampOverlay;
|
||||||
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
@ -28,13 +30,21 @@ import static com.google.common.truth.Truth.assertWithMessage;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Matrix;
|
||||||
import android.graphics.PixelFormat;
|
import android.graphics.PixelFormat;
|
||||||
import android.media.Image;
|
import android.media.Image;
|
||||||
import android.media.ImageReader;
|
import android.media.ImageReader;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.OverlaySettings;
|
||||||
|
import androidx.media3.common.PlaybackException;
|
||||||
|
import androidx.media3.common.Player;
|
||||||
|
import androidx.media3.common.VideoCompositorSettings;
|
||||||
import androidx.media3.common.util.ConditionVariable;
|
import androidx.media3.common.util.ConditionVariable;
|
||||||
import androidx.media3.common.util.Size;
|
import androidx.media3.common.util.Size;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
|
import androidx.media3.effect.MatrixTransformation;
|
||||||
|
import androidx.media3.effect.PreviewingMultipleInputVideoGraph;
|
||||||
|
import androidx.media3.effect.StaticOverlaySettings;
|
||||||
import androidx.media3.transformer.Composition;
|
import androidx.media3.transformer.Composition;
|
||||||
import androidx.media3.transformer.CompositionPlayer;
|
import androidx.media3.transformer.CompositionPlayer;
|
||||||
import androidx.media3.transformer.EditedMediaItem;
|
import androidx.media3.transformer.EditedMediaItem;
|
||||||
@ -42,6 +52,7 @@ import androidx.media3.transformer.EditedMediaItemSequence;
|
|||||||
import androidx.media3.transformer.Effects;
|
import androidx.media3.transformer.Effects;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
@ -59,8 +70,30 @@ import org.junit.runner.RunWith;
|
|||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class CompositionPlayerPixelTest {
|
public class CompositionPlayerPixelTest {
|
||||||
|
|
||||||
private static final String TEST_DIRECTORY = "test-generated-goldens/ExoPlayerPlaybackTest";
|
private static final String TEST_DIRECTORY = "test-generated-goldens/CompositionPlayerPixelTest";
|
||||||
private static final long TEST_TIMEOUT_MS = 10_000;
|
private static final long TEST_TIMEOUT_MS = 20_000;
|
||||||
|
|
||||||
|
/** Overlays non-zero-indexed frame as picture in picture. */
|
||||||
|
private static final VideoCompositorSettings TEST_COMPOSITOR_SETTINGS =
|
||||||
|
new VideoCompositorSettings() {
|
||||||
|
@Override
|
||||||
|
public Size getOutputSize(List<Size> inputSizes) {
|
||||||
|
return inputSizes.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OverlaySettings getOverlaySettings(int inputId, long presentationTimeUs) {
|
||||||
|
if (inputId == 0) {
|
||||||
|
return new StaticOverlaySettings.Builder().setAlphaScale(0.5f).build();
|
||||||
|
}
|
||||||
|
return new StaticOverlaySettings.Builder()
|
||||||
|
.setAlphaScale(0.5f)
|
||||||
|
.setBackgroundFrameAnchor(+0.9f, -0.3f)
|
||||||
|
.setScale(0.4f, 0.4f)
|
||||||
|
.setOverlayFrameAnchor(+1f, -1f)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@Rule public final TestName testName = new TestName();
|
@Rule public final TestName testName = new TestName();
|
||||||
|
|
||||||
@ -93,6 +126,7 @@ public class CompositionPlayerPixelTest {
|
|||||||
@Test
|
@Test
|
||||||
public void compositionPlayerPreviewTest_ensuresFirstFrameRenderedCorrectly() throws Exception {
|
public void compositionPlayerPreviewTest_ensuresFirstFrameRenderedCorrectly() throws Exception {
|
||||||
AtomicReference<Bitmap> renderedFirstFrameBitmap = new AtomicReference<>();
|
AtomicReference<Bitmap> renderedFirstFrameBitmap = new AtomicReference<>();
|
||||||
|
AtomicReference<PlaybackException> playerError = new AtomicReference<>();
|
||||||
ConditionVariable hasRenderedFirstFrameCondition = new ConditionVariable();
|
ConditionVariable hasRenderedFirstFrameCondition = new ConditionVariable();
|
||||||
outputImageReader =
|
outputImageReader =
|
||||||
ImageReader.newInstance(
|
ImageReader.newInstance(
|
||||||
@ -126,14 +160,27 @@ public class CompositionPlayerPixelTest {
|
|||||||
/* audioProcessors= */ ImmutableList.of(),
|
/* audioProcessors= */ ImmutableList.of(),
|
||||||
/* videoEffects= */ ImmutableList.of(
|
/* videoEffects= */ ImmutableList.of(
|
||||||
createTimestampOverlay())))
|
createTimestampOverlay())))
|
||||||
.setDurationUs(1_024_000L)
|
.setDurationUs(MP4_ASSET.videoDurationUs)
|
||||||
.build())
|
.build())
|
||||||
.build())
|
.build())
|
||||||
.build());
|
.build());
|
||||||
|
player.addListener(
|
||||||
|
new Player.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onPlayerError(PlaybackException error) {
|
||||||
|
// Unblock the main thread promptly.
|
||||||
|
hasRenderedFirstFrameCondition.open();
|
||||||
|
playerError.set(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
player.prepare();
|
player.prepare();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!hasRenderedFirstFrameCondition.block(TEST_TIMEOUT_MS)) {
|
boolean conditionOpened = hasRenderedFirstFrameCondition.block(TEST_TIMEOUT_MS);
|
||||||
|
if (playerError.get() != null) {
|
||||||
|
throw playerError.get();
|
||||||
|
}
|
||||||
|
if (!conditionOpened) {
|
||||||
throw new TimeoutException(
|
throw new TimeoutException(
|
||||||
Util.formatInvariant("First frame not rendered in %d ms.", TEST_TIMEOUT_MS));
|
Util.formatInvariant("First frame not rendered in %d ms.", TEST_TIMEOUT_MS));
|
||||||
}
|
}
|
||||||
@ -149,4 +196,213 @@ public class CompositionPlayerPixelTest {
|
|||||||
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
// TODO: b/315800590 - Verify onFirstFrameRendered is invoked only once.
|
// TODO: b/315800590 - Verify onFirstFrameRendered is invoked only once.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
compositionPlayerPreviewTest_withTwoVideoSequences_ensuresFirstFrameRenderedCorrectly()
|
||||||
|
throws Exception {
|
||||||
|
AtomicReference<Bitmap> renderedFirstFrameBitmap = new AtomicReference<>();
|
||||||
|
AtomicReference<PlaybackException> playerError = new AtomicReference<>();
|
||||||
|
ConditionVariable hasRenderedFirstFrameCondition = new ConditionVariable();
|
||||||
|
outputImageReader =
|
||||||
|
ImageReader.newInstance(
|
||||||
|
MP4_ASSET.videoFormat.width,
|
||||||
|
MP4_ASSET.videoFormat.height,
|
||||||
|
PixelFormat.RGBA_8888,
|
||||||
|
/* maxImages= */ 1);
|
||||||
|
|
||||||
|
getInstrumentation()
|
||||||
|
.runOnMainSync(
|
||||||
|
() -> {
|
||||||
|
player =
|
||||||
|
new CompositionPlayer.Builder(context)
|
||||||
|
.setPreviewingVideoGraphFactory(
|
||||||
|
new PreviewingMultipleInputVideoGraph.Factory())
|
||||||
|
.build();
|
||||||
|
outputImageReader.setOnImageAvailableListener(
|
||||||
|
imageReader -> {
|
||||||
|
try (Image image = imageReader.acquireLatestImage()) {
|
||||||
|
renderedFirstFrameBitmap.set(createArgb8888BitmapFromRgba8888Image(image));
|
||||||
|
}
|
||||||
|
hasRenderedFirstFrameCondition.open();
|
||||||
|
},
|
||||||
|
Util.createHandlerForCurrentOrMainLooper());
|
||||||
|
|
||||||
|
player.setVideoSurface(
|
||||||
|
outputImageReader.getSurface(),
|
||||||
|
new Size(MP4_ASSET.videoFormat.width, MP4_ASSET.videoFormat.height));
|
||||||
|
player.setComposition(
|
||||||
|
new Composition.Builder(
|
||||||
|
new EditedMediaItemSequence.Builder(
|
||||||
|
new EditedMediaItem.Builder(MediaItem.fromUri(MP4_ASSET.uri))
|
||||||
|
.setEffects(
|
||||||
|
new Effects(
|
||||||
|
/* audioProcessors= */ ImmutableList.of(),
|
||||||
|
/* videoEffects= */ ImmutableList.of(
|
||||||
|
createTimestampOverlay())))
|
||||||
|
.setDurationUs(MP4_ASSET.videoDurationUs)
|
||||||
|
.build())
|
||||||
|
.build(),
|
||||||
|
new EditedMediaItemSequence.Builder(
|
||||||
|
new EditedMediaItem.Builder(MediaItem.fromUri(MP4_ASSET.uri))
|
||||||
|
.setEffects(
|
||||||
|
new Effects(
|
||||||
|
/* audioProcessors= */ ImmutableList.of(),
|
||||||
|
/* videoEffects= */ ImmutableList.of(
|
||||||
|
(MatrixTransformation)
|
||||||
|
presentationTimeUs -> {
|
||||||
|
Matrix rotationMatrix = new Matrix();
|
||||||
|
rotationMatrix.postRotate(
|
||||||
|
/* degrees= */ 90);
|
||||||
|
return rotationMatrix;
|
||||||
|
})))
|
||||||
|
.setDurationUs(MP4_ASSET.videoDurationUs)
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
.setVideoCompositorSettings(TEST_COMPOSITOR_SETTINGS)
|
||||||
|
.build());
|
||||||
|
player.addListener(
|
||||||
|
new Player.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onPlayerError(PlaybackException error) {
|
||||||
|
playerError.set(error);
|
||||||
|
// Unblock the main thread promptly.
|
||||||
|
hasRenderedFirstFrameCondition.open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
player.prepare();
|
||||||
|
});
|
||||||
|
|
||||||
|
boolean conditionOpened = hasRenderedFirstFrameCondition.block(TEST_TIMEOUT_MS);
|
||||||
|
if (playerError.get() != null) {
|
||||||
|
throw playerError.get();
|
||||||
|
}
|
||||||
|
if (!conditionOpened) {
|
||||||
|
throw new TimeoutException(
|
||||||
|
Util.formatInvariant("First frame not rendered in %d ms.", TEST_TIMEOUT_MS));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertWithMessage("First frame is not rendered.")
|
||||||
|
.that(renderedFirstFrameBitmap.get())
|
||||||
|
.isNotNull();
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
getBitmapAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
/* expected= */ readBitmap(
|
||||||
|
Util.formatInvariant("%s/%s.png", TEST_DIRECTORY, testName.getMethodName())),
|
||||||
|
/* actual= */ renderedFirstFrameBitmap.get(),
|
||||||
|
testId);
|
||||||
|
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
// TODO: b/315800590 - Verify onFirstFrameRendered is invoked only once.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
compositionPlayerPreviewTest_withTwoImageSequences_ensuresFirstFrameRenderedCorrectly()
|
||||||
|
throws Exception {
|
||||||
|
AtomicReference<Bitmap> renderedFirstFrameBitmap = new AtomicReference<>();
|
||||||
|
AtomicReference<PlaybackException> playerError = new AtomicReference<>();
|
||||||
|
ConditionVariable hasRenderedFirstFrameCondition = new ConditionVariable();
|
||||||
|
outputImageReader =
|
||||||
|
ImageReader.newInstance(
|
||||||
|
PNG_ASSET.videoFormat.width,
|
||||||
|
PNG_ASSET.videoFormat.height,
|
||||||
|
PixelFormat.RGBA_8888,
|
||||||
|
/* maxImages= */ 1);
|
||||||
|
|
||||||
|
long imageDurationUs = 200_000L;
|
||||||
|
getInstrumentation()
|
||||||
|
.runOnMainSync(
|
||||||
|
() -> {
|
||||||
|
player =
|
||||||
|
new CompositionPlayer.Builder(context)
|
||||||
|
.setPreviewingVideoGraphFactory(
|
||||||
|
new PreviewingMultipleInputVideoGraph.Factory())
|
||||||
|
.build();
|
||||||
|
outputImageReader.setOnImageAvailableListener(
|
||||||
|
imageReader -> {
|
||||||
|
try (Image image = imageReader.acquireLatestImage()) {
|
||||||
|
renderedFirstFrameBitmap.set(createArgb8888BitmapFromRgba8888Image(image));
|
||||||
|
}
|
||||||
|
hasRenderedFirstFrameCondition.open();
|
||||||
|
},
|
||||||
|
Util.createHandlerForCurrentOrMainLooper());
|
||||||
|
|
||||||
|
player.setVideoSurface(
|
||||||
|
outputImageReader.getSurface(),
|
||||||
|
new Size(PNG_ASSET.videoFormat.width, PNG_ASSET.videoFormat.height));
|
||||||
|
player.setComposition(
|
||||||
|
new Composition.Builder(
|
||||||
|
new EditedMediaItemSequence.Builder(
|
||||||
|
new EditedMediaItem.Builder(
|
||||||
|
MediaItem.fromUri(PNG_ASSET.uri)
|
||||||
|
.buildUpon()
|
||||||
|
.setImageDurationMs(usToMs(imageDurationUs))
|
||||||
|
.build())
|
||||||
|
.setEffects(
|
||||||
|
new Effects(
|
||||||
|
/* audioProcessors= */ ImmutableList.of(),
|
||||||
|
/* videoEffects= */ ImmutableList.of(
|
||||||
|
createTimestampOverlay(/* textSize= */ 30))))
|
||||||
|
.setDurationUs(imageDurationUs)
|
||||||
|
.setFrameRate(30)
|
||||||
|
.build())
|
||||||
|
.build(),
|
||||||
|
new EditedMediaItemSequence.Builder(
|
||||||
|
new EditedMediaItem.Builder(
|
||||||
|
MediaItem.fromUri(PNG_ASSET.uri)
|
||||||
|
.buildUpon()
|
||||||
|
.setImageDurationMs(usToMs(imageDurationUs))
|
||||||
|
.build())
|
||||||
|
.setEffects(
|
||||||
|
new Effects(
|
||||||
|
/* audioProcessors= */ ImmutableList.of(),
|
||||||
|
/* videoEffects= */ ImmutableList.of(
|
||||||
|
(MatrixTransformation)
|
||||||
|
presentationTimeUs -> {
|
||||||
|
Matrix rotationMatrix = new Matrix();
|
||||||
|
rotationMatrix.postRotate(
|
||||||
|
/* degrees= */ 90);
|
||||||
|
return rotationMatrix;
|
||||||
|
})))
|
||||||
|
.setDurationUs(imageDurationUs)
|
||||||
|
.setFrameRate(30)
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
.setVideoCompositorSettings(TEST_COMPOSITOR_SETTINGS)
|
||||||
|
.build());
|
||||||
|
player.addListener(
|
||||||
|
new Player.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onPlayerError(PlaybackException error) {
|
||||||
|
// Unblock the main thread promptly.
|
||||||
|
hasRenderedFirstFrameCondition.open();
|
||||||
|
playerError.set(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
player.prepare();
|
||||||
|
});
|
||||||
|
|
||||||
|
boolean conditionOpened = hasRenderedFirstFrameCondition.block(TEST_TIMEOUT_MS);
|
||||||
|
if (playerError.get() != null) {
|
||||||
|
throw playerError.get();
|
||||||
|
}
|
||||||
|
if (!conditionOpened) {
|
||||||
|
throw new TimeoutException(
|
||||||
|
Util.formatInvariant("First frame not rendered in %d ms.", TEST_TIMEOUT_MS));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertWithMessage("First frame is not rendered.")
|
||||||
|
.that(renderedFirstFrameBitmap.get())
|
||||||
|
.isNotNull();
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
getBitmapAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
/* expected= */ readBitmap(
|
||||||
|
Util.formatInvariant("%s/%s.png", TEST_DIRECTORY, testName.getMethodName())),
|
||||||
|
/* actual= */ renderedFirstFrameBitmap.get(),
|
||||||
|
testId);
|
||||||
|
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
// TODO: b/315800590 - Verify onFirstFrameRendered is invoked only once.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,25 +31,37 @@ import com.google.common.collect.ImmutableList;
|
|||||||
/** Utilities for playback tests. */
|
/** Utilities for playback tests. */
|
||||||
/* package */ final class PlaybackTestUtil {
|
/* package */ final class PlaybackTestUtil {
|
||||||
|
|
||||||
|
private static final int DEFAULT_TEXT_SIZE = 300;
|
||||||
|
|
||||||
private PlaybackTestUtil() {}
|
private PlaybackTestUtil() {}
|
||||||
|
|
||||||
/** Creates an {@link OverlayEffect} that draws the timestamp onto frames. */
|
/** Creates an {@link OverlayEffect} that draws the timestamp onto frames. */
|
||||||
public static OverlayEffect createTimestampOverlay() {
|
public static OverlayEffect createTimestampOverlay() {
|
||||||
|
return createTimestampOverlay(DEFAULT_TEXT_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an {@link OverlayEffect} that draws the timestamp onto frames with a specified text
|
||||||
|
* size.
|
||||||
|
*/
|
||||||
|
public static OverlayEffect createTimestampOverlay(int textSize) {
|
||||||
return new OverlayEffect(
|
return new OverlayEffect(
|
||||||
ImmutableList.of(
|
ImmutableList.of(
|
||||||
new TimestampTextOverlay(0, -0.7f),
|
new TimestampTextOverlay(0, -0.7f, textSize),
|
||||||
new TimestampTextOverlay(0, 0),
|
new TimestampTextOverlay(0, 0, textSize),
|
||||||
new TimestampTextOverlay(0, 0.7f)));
|
new TimestampTextOverlay(0, 0.7f, textSize)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class TimestampTextOverlay extends TextOverlay {
|
private static class TimestampTextOverlay extends TextOverlay {
|
||||||
|
|
||||||
private final float x;
|
private final float x;
|
||||||
private final float y;
|
private final float y;
|
||||||
|
private final int size;
|
||||||
|
|
||||||
public TimestampTextOverlay(float x, float y) {
|
public TimestampTextOverlay(float x, float y, int size) {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
|
this.size = size;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -61,7 +73,7 @@ import com.google.common.collect.ImmutableList;
|
|||||||
text.length(),
|
text.length(),
|
||||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
text.setSpan(
|
text.setSpan(
|
||||||
new AbsoluteSizeSpan(/* size= */ 300),
|
new AbsoluteSizeSpan(size),
|
||||||
/* start= */ 0,
|
/* start= */ 0,
|
||||||
text.length(),
|
text.length(),
|
||||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user