Compositor: Add tests for 1, 3, and 5 inputs.
With this, Compositor now handles an arbitrary number of inputs! PiperOrigin-RevId: 558813361
This commit is contained in:
parent
fddb09be20
commit
350b394596
@ -61,8 +61,6 @@ public final class DefaultVideoCompositor implements VideoCompositor {
|
|||||||
// * Use a lock to synchronize inputFrameInfos more narrowly, to reduce blocking.
|
// * Use a lock to synchronize inputFrameInfos more narrowly, to reduce blocking.
|
||||||
// * If the primary stream ends, consider setting the secondary stream as the new primary stream,
|
// * If the primary stream ends, consider setting the secondary stream as the new primary stream,
|
||||||
// so that secondary stream frames aren't dropped.
|
// so that secondary stream frames aren't dropped.
|
||||||
// * Consider adding info about the timestamps for each input frame used to composite an output
|
|
||||||
// frame, to aid debugging and testing.
|
|
||||||
|
|
||||||
private static final String THREAD_NAME = "Effect:DefaultVideoCompositor:GlThread";
|
private static final String THREAD_NAME = "Effect:DefaultVideoCompositor:GlThread";
|
||||||
private static final String TAG = "DefaultVideoCompositor";
|
private static final String TAG = "DefaultVideoCompositor";
|
||||||
@ -133,6 +131,9 @@ public final class DefaultVideoCompositor implements VideoCompositor {
|
|||||||
* <p>The input source must be able to have at least two {@linkplain #queueInputTexture queued
|
* <p>The input source must be able to have at least two {@linkplain #queueInputTexture queued
|
||||||
* textures} before one texture is {@linkplain
|
* textures} before one texture is {@linkplain
|
||||||
* DefaultVideoFrameProcessor.ReleaseOutputTextureCallback released}.
|
* DefaultVideoFrameProcessor.ReleaseOutputTextureCallback released}.
|
||||||
|
*
|
||||||
|
* <p>When composited, textures are drawn in the reverse order of their registration order, so
|
||||||
|
* that the first registered source is on the very top.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public synchronized int registerInputSource() {
|
public synchronized int registerInputSource() {
|
||||||
@ -269,9 +270,7 @@ public final class DefaultVideoCompositor implements VideoCompositor {
|
|||||||
|
|
||||||
ensureGlProgramConfigured();
|
ensureGlProgramConfigured();
|
||||||
|
|
||||||
// TODO: b/262694346 -
|
// TODO: b/262694346 - Allow different input frame dimensions.
|
||||||
// * Support an arbitrary number of inputs.
|
|
||||||
// * Allow different input frame dimensions.
|
|
||||||
InputFrameInfo primaryInputFrame = framesToComposite.get(PRIMARY_INPUT_ID);
|
InputFrameInfo primaryInputFrame = framesToComposite.get(PRIMARY_INPUT_ID);
|
||||||
GlTextureInfo primaryInputTexture = primaryInputFrame.texture;
|
GlTextureInfo primaryInputTexture = primaryInputFrame.texture;
|
||||||
outputTexturePool.ensureConfigured(
|
outputTexturePool.ensureConfigured(
|
||||||
|
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.5 KiB |
@ -82,6 +82,8 @@ import org.junit.runners.Parameterized;
|
|||||||
/** Pixel test for {@link DefaultVideoCompositor} compositing 2 input frames into 1 output frame. */
|
/** Pixel test for {@link DefaultVideoCompositor} compositing 2 input frames into 1 output frame. */
|
||||||
@RunWith(Parameterized.class)
|
@RunWith(Parameterized.class)
|
||||||
public final class DefaultVideoCompositorPixelTest {
|
public final class DefaultVideoCompositorPixelTest {
|
||||||
|
// TODO: b/262694346 - Have CompositorTestRunner queueBitmapToInput queue bitmaps at specified
|
||||||
|
// timestamps instead of frame rates.
|
||||||
@Parameterized.Parameters(name = "useSharedExecutor={0}")
|
@Parameterized.Parameters(name = "useSharedExecutor={0}")
|
||||||
public static ImmutableList<Boolean> useSharedExecutor() {
|
public static ImmutableList<Boolean> useSharedExecutor() {
|
||||||
return ImmutableList.of(true, false);
|
return ImmutableList.of(true, false);
|
||||||
@ -89,7 +91,7 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
|
|
||||||
// Golden images were generated on an API 33 emulator. API 26 emulators have a different text
|
// Golden images were generated on an API 33 emulator. API 26 emulators have a different text
|
||||||
// rendering implementation that leads to a larger pixel difference.
|
// rendering implementation that leads to a larger pixel difference.
|
||||||
public static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_WITH_OVERLAY =
|
public static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_WITH_TEXT_OVERLAY =
|
||||||
(Ascii.toLowerCase(Util.DEVICE).contains("emulator")
|
(Ascii.toLowerCase(Util.DEVICE).contains("emulator")
|
||||||
|| Ascii.toLowerCase(Util.DEVICE).contains("generic"))
|
|| Ascii.toLowerCase(Util.DEVICE).contains("generic"))
|
||||||
&& SDK_INT <= 26
|
&& SDK_INT <= 26
|
||||||
@ -214,7 +216,7 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@RequiresNonNull("testId")
|
@RequiresNonNull("testId")
|
||||||
public void compositeTwoInputs_withSecondaryAlphaZero_differentTimestamp_matchesExpectedBitmap()
|
public void compositeTwoInputs_withSecondaryTransparent_differentTimestamp_matchesExpectedBitmap()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
ImmutableList<ImmutableList<Effect>> inputEffects =
|
ImmutableList<ImmutableList<Effect>> inputEffects =
|
||||||
ImmutableList.of(
|
ImmutableList.of(
|
||||||
@ -498,17 +500,101 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
assertThat(compositorTestRunner.getCompositedTimestamps()).hasSize(numberOfFramesToQueue);
|
assertThat(compositorTestRunner.getCompositedTimestamps()).hasSize(numberOfFramesToQueue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@RequiresNonNull("testId")
|
||||||
|
public void compositeFiveInputs_withFiveFramesFromEach_matchesExpectedFrameCount()
|
||||||
|
throws Exception {
|
||||||
|
compositorTestRunner =
|
||||||
|
new VideoCompositorTestRunner(
|
||||||
|
testId,
|
||||||
|
useSharedExecutor,
|
||||||
|
/* inputEffectLists= */ ImmutableList.of(
|
||||||
|
ImmutableList.of(),
|
||||||
|
ImmutableList.of(),
|
||||||
|
ImmutableList.of(),
|
||||||
|
ImmutableList.of(),
|
||||||
|
ImmutableList.of()));
|
||||||
|
int numberOfFramesToQueue = 5;
|
||||||
|
|
||||||
|
compositorTestRunner.queueBitmapToAllInputs(/* durationSec= */ numberOfFramesToQueue);
|
||||||
|
compositorTestRunner.endCompositing();
|
||||||
|
|
||||||
|
assertThat(compositorTestRunner.getCompositedTimestamps()).hasSize(numberOfFramesToQueue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@RequiresNonNull("testId")
|
||||||
|
public void compositeOneInput_matchesExpectedBitmap() throws Exception {
|
||||||
|
compositorTestRunner =
|
||||||
|
new VideoCompositorTestRunner(
|
||||||
|
testId,
|
||||||
|
useSharedExecutor,
|
||||||
|
ImmutableList.of(
|
||||||
|
ImmutableList.of(RgbFilter.createGrayscaleFilter(), new AlphaScale(100f))));
|
||||||
|
|
||||||
|
compositorTestRunner.queueBitmapToAllInputs(/* durationSec= */ 3);
|
||||||
|
compositorTestRunner.endCompositing();
|
||||||
|
|
||||||
|
ImmutableList<Long> primaryTimestamps =
|
||||||
|
ImmutableList.of(0 * C.MICROS_PER_SECOND, 1 * C.MICROS_PER_SECOND, 2 * C.MICROS_PER_SECOND);
|
||||||
|
assertThat(compositorTestRunner.inputBitmapReaders.get(0).getOutputTimestamps())
|
||||||
|
.containsExactlyElementsIn(primaryTimestamps)
|
||||||
|
.inOrder();
|
||||||
|
compositorTestRunner.saveAndAssertCompositedBitmapsMatchExpected(
|
||||||
|
ImmutableList.of("grayscale_opaque_0s", "grayscale_opaque_1s", "grayscale_opaque_2s"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@RequiresNonNull("testId")
|
||||||
|
public void compositeThreeInputs_matchesExpectedBitmap() throws Exception {
|
||||||
|
compositorTestRunner =
|
||||||
|
new VideoCompositorTestRunner(
|
||||||
|
testId,
|
||||||
|
useSharedExecutor,
|
||||||
|
ImmutableList.of(
|
||||||
|
ImmutableList.of(RgbFilter.createInvertedFilter(), new AlphaScale(0.4f)),
|
||||||
|
ImmutableList.of(RgbFilter.createGrayscaleFilter(), new AlphaScale(0.7f)),
|
||||||
|
ImmutableList.of(
|
||||||
|
new ScaleAndRotateTransformation.Builder().setRotationDegrees(180).build())));
|
||||||
|
|
||||||
|
compositorTestRunner.queueBitmapToInput(
|
||||||
|
/* inputId= */ 0, /* durationSec= */ 3, /* offsetToAddSec= */ 0, /* frameRate= */ 1);
|
||||||
|
compositorTestRunner.queueBitmapToInput(
|
||||||
|
/* inputId= */ 1, /* durationSec= */ 1, /* offsetToAddSec= */ 1, /* frameRate= */ 1);
|
||||||
|
compositorTestRunner.queueBitmapToInput(
|
||||||
|
/* inputId= */ 2, /* durationSec= */ 3, /* offsetToAddSec= */ 0, /* frameRate= */ 0.5f);
|
||||||
|
compositorTestRunner.endCompositing();
|
||||||
|
|
||||||
|
ImmutableList<Long> primaryTimestamps =
|
||||||
|
ImmutableList.of(0 * C.MICROS_PER_SECOND, 1 * C.MICROS_PER_SECOND, 2 * C.MICROS_PER_SECOND);
|
||||||
|
ImmutableList<Long> secondary1Timestamps = ImmutableList.of(1 * C.MICROS_PER_SECOND);
|
||||||
|
ImmutableList<Long> secondary2Timestamps =
|
||||||
|
ImmutableList.of(0 * C.MICROS_PER_SECOND, 2 * C.MICROS_PER_SECOND);
|
||||||
|
assertThat(compositorTestRunner.inputBitmapReaders.get(0).getOutputTimestamps())
|
||||||
|
.containsExactlyElementsIn(primaryTimestamps)
|
||||||
|
.inOrder();
|
||||||
|
assertThat(compositorTestRunner.inputBitmapReaders.get(1).getOutputTimestamps())
|
||||||
|
.containsExactlyElementsIn(secondary1Timestamps)
|
||||||
|
.inOrder();
|
||||||
|
assertThat(compositorTestRunner.inputBitmapReaders.get(2).getOutputTimestamps())
|
||||||
|
.containsExactlyElementsIn(secondary2Timestamps)
|
||||||
|
.inOrder();
|
||||||
|
assertThat(compositorTestRunner.getCompositedTimestamps())
|
||||||
|
.containsExactlyElementsIn(primaryTimestamps)
|
||||||
|
.inOrder();
|
||||||
|
compositorTestRunner.saveAndAssertCompositedBitmapsMatchExpected(
|
||||||
|
ImmutableList.of("0s_1s_0s", "1s_1s_0s", "2s_1s_2s"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A test runner for {@link DefaultVideoCompositor} tests.
|
* A test runner for {@link DefaultVideoCompositor} tests.
|
||||||
*
|
*
|
||||||
* <p>Composites input bitmaps from two input sources.
|
* <p>Composites input bitmaps from two input sources.
|
||||||
*/
|
*/
|
||||||
private static final class VideoCompositorTestRunner {
|
private static final class VideoCompositorTestRunner {
|
||||||
// Compositor tests rely on 2 VideoFrameProcessor instances, plus the compositor.
|
|
||||||
private static final int COMPOSITOR_TIMEOUT_MS = 2 * VIDEO_FRAME_PROCESSING_WAIT_MS;
|
|
||||||
private static final int COMPOSITOR_INPUT_SIZE = 2;
|
|
||||||
|
|
||||||
public final List<TextureBitmapReader> inputBitmapReaders;
|
public final List<TextureBitmapReader> inputBitmapReaders;
|
||||||
|
private final int timeoutMs;
|
||||||
private final LinkedHashMap<Long, Bitmap> outputTimestampsToBitmaps;
|
private final LinkedHashMap<Long, Bitmap> outputTimestampsToBitmaps;
|
||||||
private final List<VideoFrameProcessorTestRunner> inputVideoFrameProcessorTestRunners;
|
private final List<VideoFrameProcessorTestRunner> inputVideoFrameProcessorTestRunners;
|
||||||
private final VideoCompositor videoCompositor;
|
private final VideoCompositor videoCompositor;
|
||||||
@ -534,6 +620,7 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
ImmutableList<ImmutableList<Effect>> inputEffectLists)
|
ImmutableList<ImmutableList<Effect>> inputEffectLists)
|
||||||
throws GlUtil.GlException, VideoFrameProcessingException {
|
throws GlUtil.GlException, VideoFrameProcessingException {
|
||||||
this.testId = testId;
|
this.testId = testId;
|
||||||
|
timeoutMs = inputEffectLists.size() * VIDEO_FRAME_PROCESSING_WAIT_MS;
|
||||||
sharedExecutorService =
|
sharedExecutorService =
|
||||||
useSharedExecutor ? Util.newSingleThreadExecutor("Effect:Shared:GlThread") : null;
|
useSharedExecutor ? Util.newSingleThreadExecutor("Effect:Shared:GlThread") : null;
|
||||||
EGLContext sharedEglContext = AndroidTestUtil.createOpenGlObjects();
|
EGLContext sharedEglContext = AndroidTestUtil.createOpenGlObjects();
|
||||||
@ -578,7 +665,6 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
/* textureOutputCapacity= */ 1);
|
/* textureOutputCapacity= */ 1);
|
||||||
inputBitmapReaders = new ArrayList<>();
|
inputBitmapReaders = new ArrayList<>();
|
||||||
inputVideoFrameProcessorTestRunners = new ArrayList<>();
|
inputVideoFrameProcessorTestRunners = new ArrayList<>();
|
||||||
assertThat(inputEffectLists).hasSize(COMPOSITOR_INPUT_SIZE);
|
|
||||||
for (int i = 0; i < inputEffectLists.size(); i++) {
|
for (int i = 0; i < inputEffectLists.size(); i++) {
|
||||||
TextureBitmapReader textureBitmapReader = new TextureBitmapReader();
|
TextureBitmapReader textureBitmapReader = new TextureBitmapReader();
|
||||||
inputBitmapReaders.add(textureBitmapReader);
|
inputBitmapReaders.add(textureBitmapReader);
|
||||||
@ -629,11 +715,11 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
inputVideoFrameProcessorTestRunners.get(i).signalEndOfInput();
|
inputVideoFrameProcessorTestRunners.get(i).signalEndOfInput();
|
||||||
}
|
}
|
||||||
for (int i = 0; i < inputVideoFrameProcessorTestRunners.size(); i++) {
|
for (int i = 0; i < inputVideoFrameProcessorTestRunners.size(); i++) {
|
||||||
inputVideoFrameProcessorTestRunners.get(i).awaitFrameProcessingEnd(COMPOSITOR_TIMEOUT_MS);
|
inputVideoFrameProcessorTestRunners.get(i).awaitFrameProcessingEnd(timeoutMs);
|
||||||
}
|
}
|
||||||
@Nullable Exception endCompositingException = null;
|
@Nullable Exception endCompositingException = null;
|
||||||
try {
|
try {
|
||||||
if (!compositorEnded.await(COMPOSITOR_TIMEOUT_MS, MILLISECONDS)) {
|
if (!compositorEnded.await(timeoutMs, MILLISECONDS)) {
|
||||||
endCompositingException = new IllegalStateException("Compositing timed out.");
|
endCompositingException = new IllegalStateException("Compositing timed out.");
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
@ -685,7 +771,7 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
if (sharedExecutorService != null) {
|
if (sharedExecutorService != null) {
|
||||||
try {
|
try {
|
||||||
sharedExecutorService.shutdown();
|
sharedExecutorService.shutdown();
|
||||||
if (!sharedExecutorService.awaitTermination(COMPOSITOR_TIMEOUT_MS, MILLISECONDS)) {
|
if (!sharedExecutorService.awaitTermination(timeoutMs, MILLISECONDS)) {
|
||||||
throw new IllegalStateException("Missed shutdown timeout.");
|
throw new IllegalStateException("Missed shutdown timeout.");
|
||||||
}
|
}
|
||||||
} catch (InterruptedException unexpected) {
|
} catch (InterruptedException unexpected) {
|
||||||
@ -806,6 +892,6 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
readBitmapUnpremultipliedAlpha(expectedBitmapAssetPath), actualBitmap, testId);
|
readBitmapUnpremultipliedAlpha(expectedBitmapAssetPath), actualBitmap, testId);
|
||||||
assertWithMessage("Pixel difference for bitmapLabel = " + actualBitmapLabel)
|
assertWithMessage("Pixel difference for bitmapLabel = " + actualBitmapLabel)
|
||||||
.that(averagePixelAbsoluteDifference)
|
.that(averagePixelAbsoluteDifference)
|
||||||
.isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_WITH_OVERLAY);
|
.isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_WITH_TEXT_OVERLAY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user