Effect: Move FrameProcessorTestRunner to test-utils.

This is necessary in order to move HDR to SDR tone-mapping tests to transformer/mh.

PiperOrigin-RevId: 504858508
This commit is contained in:
huangdarwin 2023-01-26 17:08:44 +00:00 committed by christosts
parent 4206dbe73e
commit 237a98e7fb
2 changed files with 296 additions and 267 deletions

View file

@ -15,11 +15,8 @@
*/
package com.google.android.exoplayer2.effect;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.android.exoplayer2.testutil.BitmapPixelTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE;
import static com.google.android.exoplayer2.testutil.BitmapPixelTestUtil.createArgb8888BitmapFromRgba8888Image;
import static com.google.android.exoplayer2.testutil.BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888;
import static com.google.android.exoplayer2.testutil.BitmapPixelTestUtil.maybeSaveTestBitmapToCacheDirectory;
import static com.google.android.exoplayer2.testutil.BitmapPixelTestUtil.readBitmap;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
@ -28,27 +25,15 @@ import static com.google.common.truth.Truth.assertThat;
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 android.media.MediaFormat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.BitmapPixelTestUtil;
import com.google.android.exoplayer2.testutil.DecodeOneFrameUtil;
import com.google.android.exoplayer2.util.DebugViewProvider;
import com.google.android.exoplayer2.testutil.FrameProcessorTestRunner;
import com.google.android.exoplayer2.util.Effect;
import com.google.android.exoplayer2.util.FrameInfo;
import com.google.android.exoplayer2.util.FrameProcessingException;
import com.google.android.exoplayer2.util.FrameProcessor;
import com.google.android.exoplayer2.util.Size;
import com.google.android.exoplayer2.util.SurfaceInfo;
import com.google.android.exoplayer2.video.ColorInfo;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.junit.After;
import org.junit.Ignore;
@ -529,257 +514,6 @@ public final class GlEffectsFrameProcessorPixelTest {
// TODO(b/227624622): Add a test for HDR input after BitmapPixelTestUtil can read HDR bitmaps,
// using GlEffectWrapper to ensure usage of intermediate textures.
/* A test runner for {@link FrameProcessor} tests. */
private static final class FrameProcessorTestRunner {
/** A builder for {@link FrameProcessorTestRunner} instances. */
public static final class Builder {
private @MonotonicNonNull String testId;
private FrameProcessor.@MonotonicNonNull Factory frameProcessorFactory;
private @MonotonicNonNull String videoAssetPath;
private @MonotonicNonNull String outputFileLabel;
private @MonotonicNonNull ImmutableList<Effect> effects;
private float pixelWidthHeightRatio;
private @MonotonicNonNull ColorInfo inputColorInfo;
private @MonotonicNonNull ColorInfo outputColorInfo;
/** Creates a new instance with default values. */
public Builder() {
pixelWidthHeightRatio = DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO;
}
/**
* Sets the test ID, used to generate output files.
*
* <p>This is a required value.
*/
@CanIgnoreReturnValue
public Builder setTestId(String testId) {
this.testId = testId;
return this;
}
/**
* Sets the {@link FrameProcessor.Factory}.
*
* <p>This is a required value.
*/
@CanIgnoreReturnValue
public Builder setFrameProcessorFactory(FrameProcessor.Factory frameProcessorFactory) {
this.frameProcessorFactory = frameProcessorFactory;
return this;
}
/**
* Sets the input video asset path.
*
* <p>This is a required value.
*/
@CanIgnoreReturnValue
public Builder setVideoAssetPath(String videoAssetPath) {
this.videoAssetPath = videoAssetPath;
return this;
}
/**
* Sets the output file label.
*
* <p>This value will be postfixed after the {@code testId} to generated output files.
*
* <p>The default value is an empty string.
*/
@CanIgnoreReturnValue
public Builder setOutputFileLabel(String outputFileLabel) {
this.outputFileLabel = outputFileLabel;
return this;
}
/**
* Sets the effects used.
*
* <p>The default value is an empty list.
*/
@CanIgnoreReturnValue
public Builder setEffects(List<Effect> effects) {
this.effects = ImmutableList.copyOf(effects);
return this;
}
/**
* Sets the effects used.
*
* <p>The default value is an empty list.
*/
@CanIgnoreReturnValue
public Builder setEffects(Effect... effects) {
this.effects = ImmutableList.copyOf(effects);
return this;
}
/**
* Sets the {@code pixelWidthHeightRatio}.
*
* <p>The default value is {@link #DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO}.
*/
@CanIgnoreReturnValue
public Builder setPixelWidthHeightRatio(float pixelWidthHeightRatio) {
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
return this;
}
/**
* Sets the input color.
*
* <p>The default value is {@link ColorInfo#SDR_BT709_LIMITED}.
*/
@CanIgnoreReturnValue
public Builder setInputColorInfo(ColorInfo inputColorInfo) {
this.inputColorInfo = inputColorInfo;
return this;
}
/**
* Sets the output color.
*
* <p>The default value is {@link ColorInfo#SDR_BT709_LIMITED}.
*/
@CanIgnoreReturnValue
public Builder setOutputColorInfo(ColorInfo outputColorInfo) {
this.outputColorInfo = outputColorInfo;
return this;
}
public FrameProcessorTestRunner build() throws FrameProcessingException {
checkStateNotNull(testId, "testId must be set.");
checkStateNotNull(frameProcessorFactory, "frameProcessorFactory must be set.");
checkStateNotNull(videoAssetPath, "videoAssetPath must be set.");
return new FrameProcessorTestRunner(
testId,
frameProcessorFactory,
videoAssetPath,
outputFileLabel == null ? "" : outputFileLabel,
effects == null ? ImmutableList.of() : effects,
pixelWidthHeightRatio,
inputColorInfo == null ? ColorInfo.SDR_BT709_LIMITED : inputColorInfo,
outputColorInfo == null ? ColorInfo.SDR_BT709_LIMITED : outputColorInfo);
}
}
/** The ratio of width over height, for each pixel in a frame. */
private static final float DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO = 1;
/**
* Time to wait for the decoded frame to populate the {@link GlEffectsFrameProcessor} instance's
* input surface and the {@link GlEffectsFrameProcessor} to finish processing the frame, in
* milliseconds.
*/
private static final int FRAME_PROCESSING_WAIT_MS = 5000;
private final String testId;
private final String videoAssetPath;
private final String outputFileLabel;
private final float pixelWidthHeightRatio;
private final AtomicReference<FrameProcessingException> frameProcessingException;
private final FrameProcessor frameProcessor;
private volatile @MonotonicNonNull ImageReader outputImageReader;
private volatile boolean frameProcessingEnded;
private FrameProcessorTestRunner(
String testId,
FrameProcessor.Factory frameProcessorFactory,
String videoAssetPath,
String outputFileLabel,
ImmutableList<Effect> effects,
float pixelWidthHeightRatio,
ColorInfo inputColorInfo,
ColorInfo outputColorInfo)
throws FrameProcessingException {
this.testId = testId;
this.videoAssetPath = videoAssetPath;
this.outputFileLabel = outputFileLabel;
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
frameProcessingException = new AtomicReference<>();
frameProcessor =
frameProcessorFactory.create(
getApplicationContext(),
effects,
DebugViewProvider.NONE,
inputColorInfo,
outputColorInfo,
/* releaseFramesAutomatically= */ true,
MoreExecutors.directExecutor(),
new FrameProcessor.Listener() {
@Override
public void onOutputSizeChanged(int width, int height) {
outputImageReader =
ImageReader.newInstance(
width, height, PixelFormat.RGBA_8888, /* maxImages= */ 1);
checkNotNull(frameProcessor)
.setOutputSurfaceInfo(
new SurfaceInfo(outputImageReader.getSurface(), width, height));
}
@Override
public void onOutputFrameAvailable(long presentationTimeUs) {
// Do nothing as frames are released automatically.
}
@Override
public void onFrameProcessingError(FrameProcessingException exception) {
frameProcessingException.set(exception);
}
@Override
public void onFrameProcessingEnded() {
frameProcessingEnded = true;
}
});
}
public Bitmap processFirstFrameAndEnd() throws Exception {
DecodeOneFrameUtil.decodeOneAssetFileFrame(
videoAssetPath,
new DecodeOneFrameUtil.Listener() {
@Override
public void onContainerExtracted(MediaFormat mediaFormat) {
frameProcessor.setInputFrameInfo(
new FrameInfo.Builder(
mediaFormat.getInteger(MediaFormat.KEY_WIDTH),
mediaFormat.getInteger(MediaFormat.KEY_HEIGHT))
.setPixelWidthHeightRatio(pixelWidthHeightRatio)
.build());
frameProcessor.registerInputFrame();
}
@Override
public void onFrameDecoded(MediaFormat mediaFormat) {
// Do nothing.
}
},
frameProcessor.getInputSurface());
frameProcessor.signalEndOfInput();
Thread.sleep(FRAME_PROCESSING_WAIT_MS);
assertThat(frameProcessingEnded).isTrue();
assertThat(frameProcessingException.get()).isNull();
Image frameProcessorOutputImage = checkNotNull(outputImageReader).acquireLatestImage();
Bitmap actualBitmap = createArgb8888BitmapFromRgba8888Image(frameProcessorOutputImage);
frameProcessorOutputImage.close();
maybeSaveTestBitmapToCacheDirectory(testId, /* bitmapLabel= */ outputFileLabel, actualBitmap);
return actualBitmap;
}
public void release() {
if (frameProcessor != null) {
frameProcessor.release();
}
}
}
public FrameProcessorTestRunner.Builder getDefaultFrameProcessorTestRunnerBuilder(String testId) {
return new FrameProcessorTestRunner.Builder()
.setTestId(testId)

View file

@ -0,0 +1,295 @@
/*
* Copyright 2021 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 com.google.android.exoplayer2.testutil;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.android.exoplayer2.testutil.BitmapPixelTestUtil.createArgb8888BitmapFromRgba8888Image;
import static com.google.android.exoplayer2.testutil.BitmapPixelTestUtil.maybeSaveTestBitmapToCacheDirectory;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import static com.google.common.truth.Truth.assertThat;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.media.Image;
import android.media.ImageReader;
import android.media.MediaFormat;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.util.DebugViewProvider;
import com.google.android.exoplayer2.util.Effect;
import com.google.android.exoplayer2.util.FrameInfo;
import com.google.android.exoplayer2.util.FrameProcessingException;
import com.google.android.exoplayer2.util.FrameProcessor;
import com.google.android.exoplayer2.util.SurfaceInfo;
import com.google.android.exoplayer2.video.ColorInfo;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A test runner for {@link FrameProcessor} tests. */
@RequiresApi(19)
public final class FrameProcessorTestRunner {
/** A builder for {@link FrameProcessorTestRunner} instances. */
public static final class Builder {
/** The ratio of width over height, for each pixel in a frame. */
private static final float DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO = 1;
private @MonotonicNonNull String testId;
private FrameProcessor.@MonotonicNonNull Factory frameProcessorFactory;
private @MonotonicNonNull String videoAssetPath;
private @MonotonicNonNull String outputFileLabel;
private @MonotonicNonNull ImmutableList<Effect> effects;
private float pixelWidthHeightRatio;
private @MonotonicNonNull ColorInfo inputColorInfo;
private @MonotonicNonNull ColorInfo outputColorInfo;
/** Creates a new instance with default values. */
public Builder() {
pixelWidthHeightRatio = DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO;
}
/**
* Sets the test ID, used to generate output files.
*
* <p>This is a required value.
*/
@CanIgnoreReturnValue
public Builder setTestId(String testId) {
this.testId = testId;
return this;
}
/**
* Sets the {@link FrameProcessor.Factory}.
*
* <p>This is a required value.
*/
@CanIgnoreReturnValue
public Builder setFrameProcessorFactory(FrameProcessor.Factory frameProcessorFactory) {
this.frameProcessorFactory = frameProcessorFactory;
return this;
}
/**
* Sets the input video asset path.
*
* <p>This is a required value.
*/
@CanIgnoreReturnValue
public Builder setVideoAssetPath(String videoAssetPath) {
this.videoAssetPath = videoAssetPath;
return this;
}
/**
* Sets the output file label.
*
* <p>This value will be postfixed after the {@code testId} to generated output files.
*
* <p>The default value is an empty string.
*/
@CanIgnoreReturnValue
public Builder setOutputFileLabel(String outputFileLabel) {
this.outputFileLabel = outputFileLabel;
return this;
}
/**
* Sets the {@link Effect}s used.
*
* <p>The default value is an empty list.
*/
@CanIgnoreReturnValue
public Builder setEffects(List<Effect> effects) {
this.effects = ImmutableList.copyOf(effects);
return this;
}
/**
* Sets the {@link Effect}s used.
*
* <p>The default value is an empty list.
*/
@CanIgnoreReturnValue
public Builder setEffects(Effect... effects) {
this.effects = ImmutableList.copyOf(effects);
return this;
}
/**
* Sets the {@code pixelWidthHeightRatio}.
*
* <p>The default value is {@link #DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO}.
*/
@CanIgnoreReturnValue
public Builder setPixelWidthHeightRatio(float pixelWidthHeightRatio) {
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
return this;
}
/**
* Sets the input {@link ColorInfo}.
*
* <p>The default value is {@link ColorInfo#SDR_BT709_LIMITED}.
*/
@CanIgnoreReturnValue
public Builder setInputColorInfo(ColorInfo inputColorInfo) {
this.inputColorInfo = inputColorInfo;
return this;
}
/**
* Sets the output {@link ColorInfo}.
*
* <p>The default value is {@link ColorInfo#SDR_BT709_LIMITED}.
*/
@CanIgnoreReturnValue
public Builder setOutputColorInfo(ColorInfo outputColorInfo) {
this.outputColorInfo = outputColorInfo;
return this;
}
public FrameProcessorTestRunner build() throws FrameProcessingException {
checkStateNotNull(testId, "testId must be set.");
checkStateNotNull(frameProcessorFactory, "frameProcessorFactory must be set.");
checkStateNotNull(videoAssetPath, "videoAssetPath must be set.");
return new FrameProcessorTestRunner(
testId,
frameProcessorFactory,
videoAssetPath,
outputFileLabel == null ? "" : outputFileLabel,
effects == null ? ImmutableList.of() : effects,
pixelWidthHeightRatio,
inputColorInfo == null ? ColorInfo.SDR_BT709_LIMITED : inputColorInfo,
outputColorInfo == null ? ColorInfo.SDR_BT709_LIMITED : outputColorInfo);
}
}
/**
* Time to wait for the decoded frame to populate the {@link FrameProcessor} instance's input
* surface and the {@link FrameProcessor} to finish processing the frame, in milliseconds.
*/
private static final int FRAME_PROCESSING_WAIT_MS = 5000;
private final String testId;
private final String videoAssetPath;
private final String outputFileLabel;
private final float pixelWidthHeightRatio;
private final AtomicReference<FrameProcessingException> frameProcessingException;
private final FrameProcessor frameProcessor;
private volatile @MonotonicNonNull ImageReader outputImageReader;
private volatile boolean frameProcessingEnded;
private FrameProcessorTestRunner(
String testId,
FrameProcessor.Factory frameProcessorFactory,
String videoAssetPath,
String outputFileLabel,
ImmutableList<Effect> effects,
float pixelWidthHeightRatio,
ColorInfo inputColorInfo,
ColorInfo outputColorInfo)
throws FrameProcessingException {
this.testId = testId;
this.videoAssetPath = videoAssetPath;
this.outputFileLabel = outputFileLabel;
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
frameProcessingException = new AtomicReference<>();
frameProcessor =
frameProcessorFactory.create(
getApplicationContext(),
effects,
DebugViewProvider.NONE,
inputColorInfo,
outputColorInfo,
/* releaseFramesAutomatically= */ true,
MoreExecutors.directExecutor(),
new FrameProcessor.Listener() {
@Override
public void onOutputSizeChanged(int width, int height) {
outputImageReader =
ImageReader.newInstance(
width, height, PixelFormat.RGBA_8888, /* maxImages= */ 1);
checkNotNull(frameProcessor)
.setOutputSurfaceInfo(
new SurfaceInfo(outputImageReader.getSurface(), width, height));
}
@Override
public void onOutputFrameAvailable(long presentationTimeUs) {
// Do nothing as frames are released automatically.
}
@Override
public void onFrameProcessingError(FrameProcessingException exception) {
frameProcessingException.set(exception);
}
@Override
public void onFrameProcessingEnded() {
frameProcessingEnded = true;
}
});
}
public Bitmap processFirstFrameAndEnd() throws Exception {
DecodeOneFrameUtil.decodeOneAssetFileFrame(
videoAssetPath,
new DecodeOneFrameUtil.Listener() {
@Override
public void onContainerExtracted(MediaFormat mediaFormat) {
frameProcessor.setInputFrameInfo(
new FrameInfo.Builder(
mediaFormat.getInteger(MediaFormat.KEY_WIDTH),
mediaFormat.getInteger(MediaFormat.KEY_HEIGHT))
.setPixelWidthHeightRatio(pixelWidthHeightRatio)
.build());
frameProcessor.registerInputFrame();
}
@Override
public void onFrameDecoded(MediaFormat mediaFormat) {
// Do nothing.
}
},
frameProcessor.getInputSurface());
frameProcessor.signalEndOfInput();
Thread.sleep(FRAME_PROCESSING_WAIT_MS);
assertThat(frameProcessingEnded).isTrue();
assertThat(frameProcessingException.get()).isNull();
Image frameProcessorOutputImage = checkNotNull(outputImageReader).acquireLatestImage();
Bitmap actualBitmap = createArgb8888BitmapFromRgba8888Image(frameProcessorOutputImage);
frameProcessorOutputImage.close();
maybeSaveTestBitmapToCacheDirectory(testId, /* bitmapLabel= */ outputFileLabel, actualBitmap);
return actualBitmap;
}
public void release() {
if (frameProcessor != null) {
frameProcessor.release();
}
}
}