Flush: VideoFrameProcessor Image Input.

Much simpler than ExternalTextureManager flushing, because
ExternalTextureManager complexity is due to decoder race condition behavior,
which we won't see for Bitmaps due to a more well-defined interface.

This is needed to test texture output flushing.

PiperOrigin-RevId: 570896363
This commit is contained in:
huangdarwin 2023-10-04 21:17:45 -07:00 committed by Copybara-Service
parent a03e20fe6c
commit 89a1bb528d
4 changed files with 159 additions and 5 deletions

View File

@ -0,0 +1,131 @@
/*
* Copyright 2023 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
*
* https://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.effect;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmapUnpremultipliedAlpha;
import static com.google.common.truth.Truth.assertThat;
import android.graphics.Bitmap;
import androidx.media3.common.C;
import androidx.media3.common.ColorInfo;
import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.test.utils.VideoFrameProcessorTestRunner;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
/** Test for {@link DefaultVideoFrameProcessor} flushing. */
@RunWith(AndroidJUnit4.class)
public class DefaultVideoFrameProcessorFlushTest {
private static final String ORIGINAL_PNG_ASSET_PATH =
"media/bitmap/input_images/media3test_srgb.png";
@Rule public final TestName testName = new TestName();
private int outputFrameCount;
private @MonotonicNonNull String testId;
private @MonotonicNonNull VideoFrameProcessorTestRunner videoFrameProcessorTestRunner;
@Before
@EnsuresNonNull({"testId"})
public void setUp() {
testId = testName.getMethodName();
}
@After
public void release() {
checkNotNull(videoFrameProcessorTestRunner).release();
}
@Test
@RequiresNonNull({"testId"})
public void imageInput_flushBeforeInput_outputsAllFrames() throws Exception {
videoFrameProcessorTestRunner = createDefaultVideoFrameProcessorTestRunner(testId);
Bitmap bitmap = readBitmapUnpremultipliedAlpha(ORIGINAL_PNG_ASSET_PATH);
int inputFrameCount = 3;
videoFrameProcessorTestRunner.flush();
videoFrameProcessorTestRunner.queueInputBitmap(
bitmap,
/* durationUs= */ inputFrameCount * C.MICROS_PER_SECOND,
/* offsetToAddUs= */ 0L,
/* frameRate= */ 1);
videoFrameProcessorTestRunner.endFrameProcessing();
assertThat(outputFrameCount).isEqualTo(inputFrameCount);
}
// This tests a condition that is difficult to synchronize, and is subject to a race condition. It
// may flake/fail if any queued frames are processed in the VideoFrameProcessor thread, before
// flush begins and cancels these pending frames. However, this is better than not testing this
// behavior at all, and in practice has succeeded every time on a 1000-time run.
// TODO: b/302695659 - Make this test more deterministic.
@Test
@RequiresNonNull({"testId"})
public void imageInput_flushRightAfterInput_outputsPartialFrames() throws Exception {
videoFrameProcessorTestRunner = createDefaultVideoFrameProcessorTestRunner(testId);
Bitmap bitmap = readBitmapUnpremultipliedAlpha(ORIGINAL_PNG_ASSET_PATH);
int inputFrameCount = 3;
videoFrameProcessorTestRunner.queueInputBitmap(
bitmap,
/* durationUs= */ inputFrameCount * C.MICROS_PER_SECOND,
/* offsetToAddUs= */ 0L,
/* frameRate= */ 1);
videoFrameProcessorTestRunner.flush();
videoFrameProcessorTestRunner.endFrameProcessing();
// This assertion is subject to flaking, per test comments. If it flakes, consider increasing
// inputFrameCount.
assertThat(outputFrameCount).isLessThan(inputFrameCount);
}
@Test
@RequiresNonNull({"testId"})
public void imageInput_flushAfterAllFramesOutput_outputsAllFrames() throws Exception {
videoFrameProcessorTestRunner = createDefaultVideoFrameProcessorTestRunner(testId);
Bitmap bitmap = readBitmapUnpremultipliedAlpha(ORIGINAL_PNG_ASSET_PATH);
int inputFrameCount = 3;
videoFrameProcessorTestRunner.queueInputBitmap(
bitmap,
/* durationUs= */ inputFrameCount * C.MICROS_PER_SECOND,
/* offsetToAddUs= */ 0L,
/* frameRate= */ 1);
videoFrameProcessorTestRunner.endFrameProcessing();
videoFrameProcessorTestRunner.flush();
assertThat(outputFrameCount).isEqualTo(inputFrameCount);
}
private VideoFrameProcessorTestRunner createDefaultVideoFrameProcessorTestRunner(String testId)
throws VideoFrameProcessingException {
return new VideoFrameProcessorTestRunner.Builder()
.setTestId(testId)
.setVideoFrameProcessorFactory(new DefaultVideoFrameProcessor.Factory.Builder().build())
.setInputColorInfo(ColorInfo.SRGB_BT709_FULL)
.setOnOutputFrameAvailableForRenderingListener(unused -> outputFrameCount++)
.build();
}
}

View File

@ -61,6 +61,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private boolean currentInputStreamEnded; private boolean currentInputStreamEnded;
private boolean isNextFrameInTexture; private boolean isNextFrameInTexture;
@Nullable private volatile VideoFrameProcessingTaskExecutor.Task onFlushCompleteTask;
/** /**
* Creates a new instance. * Creates a new instance.
* *
@ -89,6 +91,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}); });
} }
@Override
public void onFlush() {
videoFrameProcessingTaskExecutor.submit(this::flush);
}
@Override @Override
public void queueInputBitmap( public void queueInputBitmap(
Bitmap inputBitmap, Bitmap inputBitmap,
@ -124,7 +131,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public void setOnFlushCompleteListener(@Nullable VideoFrameProcessingTaskExecutor.Task task) { public void setOnFlushCompleteListener(@Nullable VideoFrameProcessingTaskExecutor.Task task) {
// Do nothing. onFlushCompleteTask = task;
} }
@Override @Override
@ -192,6 +199,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
private void flush() {
pendingBitmaps.clear();
if (onFlushCompleteTask != null) {
videoFrameProcessingTaskExecutor.submitWithHighPriority(onFlushCompleteTask);
}
}
/** Information needed to generate all the frames associated with a specific {@link Bitmap}. */ /** Information needed to generate all the frames associated with a specific {@link Bitmap}. */
private static final class BitmapFrameSequenceInfo { private static final class BitmapFrameSequenceInfo {
public final Bitmap bitmap; public final Bitmap bitmap;

View File

@ -382,9 +382,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
if (pendingInputStreamInfo != null) { if (pendingInputStreamInfo != null) {
InputStreamInfo pendingInputStreamInfo = this.pendingInputStreamInfo; InputStreamInfo pendingInputStreamInfo = this.pendingInputStreamInfo;
videoFrameProcessingTaskExecutor.submit( videoFrameProcessingTaskExecutor.submit(
() -> { () -> configureEffects(pendingInputStreamInfo, /* forceReconfigure= */ false));
configureEffects(pendingInputStreamInfo, /* forceReconfigure= */ false);
});
this.pendingInputStreamInfo = null; this.pendingInputStreamInfo = null;
} }
} }
@ -551,6 +549,9 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
@Override @Override
public void flush() { public void flush() {
if (!inputSwitcher.hasActiveInput()) {
return;
}
try { try {
videoFrameProcessingTaskExecutor.flush(); videoFrameProcessingTaskExecutor.flush();
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);

View File

@ -205,6 +205,9 @@ public final class VideoFrameProcessorTestRunner {
* Sets the method to be called in {@link * Sets the method to be called in {@link
* VideoFrameProcessor.Listener#onOutputFrameAvailableForRendering}. * VideoFrameProcessor.Listener#onOutputFrameAvailableForRendering}.
* *
* <p>The method will be called on the thread the {@link VideoFrameProcessorTestRunner} is
* created on.
*
* <p>The default value is a no-op. * <p>The default value is a no-op.
*/ */
@CanIgnoreReturnValue @CanIgnoreReturnValue
@ -290,7 +293,7 @@ public final class VideoFrameProcessorTestRunner {
inputColorInfo, inputColorInfo,
outputColorInfo, outputColorInfo,
/* renderFramesAutomatically= */ true, /* renderFramesAutomatically= */ true,
MoreExecutors.directExecutor(), /* listenerExecutor= */ MoreExecutors.directExecutor(),
new VideoFrameProcessor.Listener() { new VideoFrameProcessor.Listener() {
@Override @Override
public void onInputStreamRegistered( public void onInputStreamRegistered(
@ -444,6 +447,11 @@ public final class VideoFrameProcessorTestRunner {
videoFrameProcessor.signalEndOfInput(); videoFrameProcessor.signalEndOfInput();
} }
/** Calls {@link VideoFrameProcessor#flush}. */
public void flush() {
videoFrameProcessor.flush();
}
/** After {@link #signalEndOfInput}, is called, wait for this instance to end. */ /** After {@link #signalEndOfInput}, is called, wait for this instance to end. */
public void awaitFrameProcessingEnd(long videoFrameProcessingWaitTimeMs) { public void awaitFrameProcessingEnd(long videoFrameProcessingWaitTimeMs) {
@Nullable Exception endFrameProcessingException = null; @Nullable Exception endFrameProcessingException = null;