diff --git a/libraries/test_data/src/test/assets/test-generated-goldens/CompositionPlayerPixelTest/compositionPlayerPreviewTest_withTwoImageSequences_ensuresFirstFrameRenderedCorrectly.png b/libraries/test_data/src/test/assets/test-generated-goldens/CompositionPlayerPixelTest/compositionPlayerPreviewTest_withTwoImageSequences_ensuresFirstFrameRenderedCorrectly.png new file mode 100644 index 0000000000..ebc5709bcf Binary files /dev/null and b/libraries/test_data/src/test/assets/test-generated-goldens/CompositionPlayerPixelTest/compositionPlayerPreviewTest_withTwoImageSequences_ensuresFirstFrameRenderedCorrectly.png differ diff --git a/libraries/test_data/src/test/assets/test-generated-goldens/CompositionPlayerPixelTest/compositionPlayerPreviewTest_withTwoVideoSequences_ensuresFirstFrameRenderedCorrectly.png b/libraries/test_data/src/test/assets/test-generated-goldens/CompositionPlayerPixelTest/compositionPlayerPreviewTest_withTwoVideoSequences_ensuresFirstFrameRenderedCorrectly.png new file mode 100644 index 0000000000..94026d608f Binary files /dev/null and b/libraries/test_data/src/test/assets/test-generated-goldens/CompositionPlayerPixelTest/compositionPlayerPreviewTest_withTwoVideoSequences_ensuresFirstFrameRenderedCorrectly.png differ diff --git a/libraries/test_data/src/test/assets/test-generated-goldens/CompositionPlayerPixelTest/first_frame.png b/libraries/test_data/src/test/assets/test-generated-goldens/CompositionPlayerPixelTest/first_frame.png new file mode 100644 index 0000000000..f45e0be6ce Binary files /dev/null and b/libraries/test_data/src/test/assets/test-generated-goldens/CompositionPlayerPixelTest/first_frame.png differ diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionMultipleSequencePlaybackTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionMultipleSequencePlaybackTest.java new file mode 100644 index 0000000000..e422de4914 --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionMultipleSequencePlaybackTest.java @@ -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 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 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 expectedTimestampsUs = + new ImmutableList.Builder() + .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 expectedTimestampsUs = + new ImmutableList.Builder() + .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 expectedTimestampsUs = + new ImmutableList.Builder() + .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 expectedTimestampsUs = + new ImmutableList.Builder() + .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(); + } +} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/CompositionPlayerPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/CompositionPlayerPixelTest.java index 4b4ec99be8..1be08e2c5f 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/CompositionPlayerPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/CompositionPlayerPixelTest.java @@ -16,11 +16,13 @@ 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.createArgb8888BitmapFromRgba8888Image; import static androidx.media3.test.utils.BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888; import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap; 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.test.platform.app.InstrumentationRegistry.getInstrumentation; 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.graphics.Bitmap; +import android.graphics.Matrix; import android.graphics.PixelFormat; import android.media.Image; import android.media.ImageReader; 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.Size; 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.CompositionPlayer; import androidx.media3.transformer.EditedMediaItem; @@ -42,6 +52,7 @@ import androidx.media3.transformer.EditedMediaItemSequence; import androidx.media3.transformer.Effects; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; +import java.util.List; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -59,8 +70,30 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class CompositionPlayerPixelTest { - private static final String TEST_DIRECTORY = "test-generated-goldens/ExoPlayerPlaybackTest"; - private static final long TEST_TIMEOUT_MS = 10_000; + private static final String TEST_DIRECTORY = "test-generated-goldens/CompositionPlayerPixelTest"; + 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 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(); @@ -93,6 +126,7 @@ public class CompositionPlayerPixelTest { @Test public void compositionPlayerPreviewTest_ensuresFirstFrameRenderedCorrectly() throws Exception { AtomicReference renderedFirstFrameBitmap = new AtomicReference<>(); + AtomicReference playerError = new AtomicReference<>(); ConditionVariable hasRenderedFirstFrameCondition = new ConditionVariable(); outputImageReader = ImageReader.newInstance( @@ -126,14 +160,27 @@ public class CompositionPlayerPixelTest { /* audioProcessors= */ ImmutableList.of(), /* videoEffects= */ ImmutableList.of( createTimestampOverlay()))) - .setDurationUs(1_024_000L) + .setDurationUs(MP4_ASSET.videoDurationUs) .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(); }); - 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( 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); // TODO: b/315800590 - Verify onFirstFrameRendered is invoked only once. } + + @Test + public void + compositionPlayerPreviewTest_withTwoVideoSequences_ensuresFirstFrameRenderedCorrectly() + throws Exception { + AtomicReference renderedFirstFrameBitmap = new AtomicReference<>(); + AtomicReference 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 renderedFirstFrameBitmap = new AtomicReference<>(); + AtomicReference 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. + } } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/PlaybackTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/PlaybackTestUtil.java index 1ee7076641..6fc4d9ef33 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/PlaybackTestUtil.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/PlaybackTestUtil.java @@ -31,25 +31,37 @@ import com.google.common.collect.ImmutableList; /** Utilities for playback tests. */ /* package */ final class PlaybackTestUtil { + private static final int DEFAULT_TEXT_SIZE = 300; + private PlaybackTestUtil() {} /** Creates an {@link OverlayEffect} that draws the timestamp onto frames. */ 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( ImmutableList.of( - new TimestampTextOverlay(0, -0.7f), - new TimestampTextOverlay(0, 0), - new TimestampTextOverlay(0, 0.7f))); + new TimestampTextOverlay(0, -0.7f, textSize), + new TimestampTextOverlay(0, 0, textSize), + new TimestampTextOverlay(0, 0.7f, textSize))); } private static class TimestampTextOverlay extends TextOverlay { private final float x; 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.y = y; + this.size = size; } @Override @@ -61,7 +73,7 @@ import com.google.common.collect.ImmutableList; text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); text.setSpan( - new AbsoluteSizeSpan(/* size= */ 300), + new AbsoluteSizeSpan(size), /* start= */ 0, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);