Add tests for multi input video

PiperOrigin-RevId: 716208222
This commit is contained in:
claincly 2025-01-16 06:16:04 -08:00 committed by Copybara-Service
parent b2c31b0743
commit 1732892927
6 changed files with 493 additions and 9 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 KiB

View File

@ -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();
}
}

View File

@ -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.
}
} }

View File

@ -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);