FrameProcessor: Create PresentationFrameProcessor.

Allow apps to modify how frames are presented, via modifying resolution.

A follow-up CL will provide aspect ratio, cropping, etc.

PiperOrigin-RevId: 436963312
This commit is contained in:
huangdarwin 2022-03-24 11:42:05 +00:00 committed by Ian Baker
parent c5e8503e2c
commit 5bc94da16a
8 changed files with 347 additions and 166 deletions

View File

@ -178,14 +178,12 @@ public final class FrameProcessorChainPixelTest {
} }
@Test @Test
public void processData_withScaleToFitFrameProcessor_requestOutputHeight_producesExpectedOutput() public void
processData_withPresentationFrameProcessor_requestOutputHeight_producesExpectedOutput()
throws Exception { throws Exception {
String testId = "processData_withScaleToFitFrameProcessor_requestOutputHeight"; String testId = "processData_withPresentationFrameProcessor_requestOutputHeight";
// TODO(b/213190310): After creating a Presentation class, move VideoSamplePipeline
// resolution-based adjustments (ex. in cl/419619743) to that Presentation class, so we can
// test that rotation doesn't distort the image.
GlFrameProcessor glFrameProcessor = GlFrameProcessor glFrameProcessor =
new ScaleToFitFrameProcessor.Builder(getApplicationContext()).setResolution(480).build(); new PresentationFrameProcessor.Builder(getApplicationContext()).setResolution(480).build();
setUpAndPrepareFirstFrame(glFrameProcessor); setUpAndPrepareFirstFrame(glFrameProcessor);
Bitmap expectedBitmap = Bitmap expectedBitmap =
BitmapTestUtil.readBitmap(REQUEST_OUTPUT_HEIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING); BitmapTestUtil.readBitmap(REQUEST_OUTPUT_HEIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING);
@ -205,9 +203,6 @@ public final class FrameProcessorChainPixelTest {
public void processData_withScaleToFitFrameProcessor_rotate45_producesExpectedOutput() public void processData_withScaleToFitFrameProcessor_rotate45_producesExpectedOutput()
throws Exception { throws Exception {
String testId = "processData_withScaleToFitFrameProcessor_rotate45"; String testId = "processData_withScaleToFitFrameProcessor_rotate45";
// TODO(b/213190310): After creating a Presentation class, move VideoSamplePipeline
// resolution-based adjustments (ex. in cl/419619743) to that Presentation class, so we can
// test that rotation doesn't distort the image.
GlFrameProcessor glFrameProcessor = GlFrameProcessor glFrameProcessor =
new ScaleToFitFrameProcessor.Builder(getApplicationContext()) new ScaleToFitFrameProcessor.Builder(getApplicationContext())
.setRotationDegrees(45) .setRotationDegrees(45)

View File

@ -98,7 +98,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/ */
public AdvancedFrameProcessor(Context context, Matrix transformationMatrix) { public AdvancedFrameProcessor(Context context, Matrix transformationMatrix) {
this.context = context; this.context = context;
this.transformationMatrix = transformationMatrix; this.transformationMatrix = new Matrix(transformationMatrix);
} }
@Override @Override

View File

@ -0,0 +1,182 @@
/*
* Copyright 2022 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.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.graphics.Matrix;
import android.util.Size;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.util.GlUtil;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Controls how a frame is viewed, by changing resolution. */
// TODO(b/213190310): Implement crop, aspect ratio changes, etc.
/* package */ final class PresentationFrameProcessor implements GlFrameProcessor {
/** A builder for {@link PresentationFrameProcessor} instances. */
public static final class Builder {
// Mandatory field.
private final Context context;
// Optional field.
private int outputHeight;
/**
* Creates a builder with default values.
*
* @param context The {@link Context}.
*/
public Builder(Context context) {
this.context = context;
outputHeight = C.LENGTH_UNSET;
}
/**
* Sets the output resolution using the output height.
*
* <p>The default value {@link C#LENGTH_UNSET} corresponds to using the same height as the
* input. Output width of the displayed frame will scale to preserve the frame's aspect ratio
* after other transformations.
*
* <p>For example, a 1920x1440 frame can be scaled to 640x480 by calling setResolution(480).
*
* @param outputHeight The output height of the displayed frame, in pixels.
* @return This builder.
*/
public Builder setResolution(int outputHeight) {
this.outputHeight = outputHeight;
return this;
}
public PresentationFrameProcessor build() {
return new PresentationFrameProcessor(context, outputHeight);
}
}
static {
GlUtil.glAssertionsEnabled = true;
}
private final Context context;
private final int requestedHeight;
private @MonotonicNonNull AdvancedFrameProcessor advancedFrameProcessor;
private int inputWidth;
private int inputHeight;
private int outputHeight;
private int outputRotationDegrees;
private @MonotonicNonNull Matrix transformationMatrix;
/**
* Creates a new instance.
*
* @param context The {@link Context}.
* @param requestedHeight The height of the output frame, in pixels.
*/
private PresentationFrameProcessor(Context context, int requestedHeight) {
this.context = context;
this.requestedHeight = requestedHeight;
inputWidth = C.LENGTH_UNSET;
inputHeight = C.LENGTH_UNSET;
outputHeight = C.LENGTH_UNSET;
outputRotationDegrees = C.LENGTH_UNSET;
}
/**
* Returns {@link Format#rotationDegrees} for the output frame.
*
* <p>Return values may be {@code 0} or {@code 90} degrees.
*
* <p>This method can only be called after {@link #configureOutputSize(int, int)}.
*/
public int getOutputRotationDegrees() {
checkState(outputRotationDegrees != C.LENGTH_UNSET);
return outputRotationDegrees;
}
/**
* Returns whether this {@code PresentationFrameProcessor} will apply any changes on a frame.
*
* <p>The {@code PresentationFrameProcessor} should only be used if this returns true.
*
* <p>This method can only be called after {@link #configureOutputSize(int, int)}.
*/
public boolean shouldProcess() {
checkStateNotNull(transformationMatrix);
return inputHeight != outputHeight || !transformationMatrix.isIdentity();
}
@Override
public Size configureOutputSize(int inputWidth, int inputHeight) {
this.inputWidth = inputWidth;
this.inputHeight = inputHeight;
transformationMatrix = new Matrix();
int displayWidth = inputWidth;
int displayHeight = inputHeight;
// Scale width and height to desired requestedHeight, preserving aspect ratio.
if (requestedHeight != C.LENGTH_UNSET && requestedHeight != displayHeight) {
displayWidth = Math.round((float) requestedHeight * displayWidth / displayHeight);
displayHeight = requestedHeight;
}
int outputWidth;
// Encoders commonly support higher maximum widths than maximum heights. Rotate the decoded
// frame before encoding, so the encoded frame's width >= height, and set
// outputRotationDegrees to ensure the frame is displayed in the correct orientation.
if (displayHeight > displayWidth) {
outputRotationDegrees = 90;
outputWidth = displayHeight;
outputHeight = displayWidth;
// TODO(b/201293185): After fragment shader transformations are implemented, put postRotate in
// a later GlFrameProcessor.
transformationMatrix.postRotate(outputRotationDegrees);
} else {
outputRotationDegrees = 0;
outputWidth = displayWidth;
outputHeight = displayHeight;
}
return new Size(outputWidth, outputHeight);
}
@Override
public void initialize(int inputTexId) throws IOException {
checkStateNotNull(transformationMatrix);
advancedFrameProcessor = new AdvancedFrameProcessor(context, transformationMatrix);
advancedFrameProcessor.configureOutputSize(inputWidth, inputHeight);
advancedFrameProcessor.initialize(inputTexId);
}
@Override
public void updateProgramAndDraw(long presentationTimeNs) {
checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeNs);
}
@Override
public void release() {
if (advancedFrameProcessor != null) {
advancedFrameProcessor.release();
}
}
}

View File

@ -15,7 +15,6 @@
*/ */
package androidx.media3.transformer; package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
@ -24,14 +23,13 @@ import android.content.Context;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.util.Size; import android.util.Size;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.GlUtil;
import java.io.IOException; import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* Applies a simple rotation and/or scale in the vertex shader. All input frames' pixels will be * Applies a simple rotation and/or scale in the vertex shader. All input frames' pixels will be
* preserved, potentially changing the width and height of the video by scaling dimensions to fit. * preserved, potentially changing the width and height of the frame by scaling dimensions to fit.
* The background color will default to black. * The background color will default to black.
*/ */
/* package */ final class ScaleToFitFrameProcessor implements GlFrameProcessor { /* package */ final class ScaleToFitFrameProcessor implements GlFrameProcessor {
@ -41,8 +39,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// Mandatory field. // Mandatory field.
private final Context context; private final Context context;
// Optional field. // Optional fields.
private int outputHeight;
private float scaleX; private float scaleX;
private float scaleY; private float scaleY;
private float rotationDegrees; private float rotationDegrees;
@ -55,7 +52,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public Builder(Context context) { public Builder(Context context) {
this.context = context; this.context = context;
outputHeight = C.LENGTH_UNSET;
scaleX = 1; scaleX = 1;
scaleY = 1; scaleY = 1;
rotationDegrees = 0; rotationDegrees = 0;
@ -89,25 +85,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return this; return this;
} }
/**
* Sets the output resolution using the output height.
*
* <p>The default value {@link C#LENGTH_UNSET} corresponds to using the same height as the
* input. Output width of the displayed frame will scale to preserve the frame's aspect ratio
* after other transformations.
*
* <p>For example, a 1920x1440 frame can be scaled to 640x480 by calling setResolution(480).
*
* @param outputHeight The output height of the displayed frame, in pixels.
* @return This builder.
*/
public Builder setResolution(int outputHeight) {
this.outputHeight = outputHeight;
return this;
}
public ScaleToFitFrameProcessor build() { public ScaleToFitFrameProcessor build() {
return new ScaleToFitFrameProcessor(context, scaleX, scaleY, rotationDegrees, outputHeight); return new ScaleToFitFrameProcessor(context, scaleX, scaleY, rotationDegrees);
} }
} }
@ -117,14 +96,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final Context context; private final Context context;
private final Matrix transformationMatrix; private final Matrix transformationMatrix;
private final int requestedHeight;
private @MonotonicNonNull AdvancedFrameProcessor advancedFrameProcessor; private @MonotonicNonNull AdvancedFrameProcessor advancedFrameProcessor;
private int inputWidth; private int inputWidth;
private int inputHeight; private int inputHeight;
private int outputWidth;
private int outputHeight;
private int outputRotationDegrees;
private @MonotonicNonNull Matrix adjustedTransformationMatrix; private @MonotonicNonNull Matrix adjustedTransformationMatrix;
/** /**
@ -134,34 +109,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* @param scaleX The multiplier by which the frame will scale horizontally, along the x-axis. * @param scaleX The multiplier by which the frame will scale horizontally, along the x-axis.
* @param scaleY The multiplier by which the frame will scale vertically, along the y-axis. * @param scaleY The multiplier by which the frame will scale vertically, along the y-axis.
* @param rotationDegrees How much to rotate the frame counterclockwise, in degrees. * @param rotationDegrees How much to rotate the frame counterclockwise, in degrees.
* @param requestedHeight The height of the output frame, in pixels.
*/ */
private ScaleToFitFrameProcessor( private ScaleToFitFrameProcessor(
Context context, float scaleX, float scaleY, float rotationDegrees, int requestedHeight) { Context context, float scaleX, float scaleY, float rotationDegrees) {
this.context = context; this.context = context;
this.transformationMatrix = new Matrix(); this.transformationMatrix = new Matrix();
this.transformationMatrix.postScale(scaleX, scaleY); this.transformationMatrix.postScale(scaleX, scaleY);
this.transformationMatrix.postRotate(rotationDegrees); this.transformationMatrix.postRotate(rotationDegrees);
this.requestedHeight = requestedHeight;
inputWidth = C.LENGTH_UNSET; inputWidth = C.LENGTH_UNSET;
inputHeight = C.LENGTH_UNSET; inputHeight = C.LENGTH_UNSET;
outputWidth = C.LENGTH_UNSET;
outputHeight = C.LENGTH_UNSET;
outputRotationDegrees = C.LENGTH_UNSET;
}
/**
* Returns {@link Format#rotationDegrees} for the output frame.
*
* <p>Return values may be {@code 0} or {@code 90} degrees.
*
* <p>This method can only be called after {@link #configureOutputSize(int, int)}.
*/
public int getOutputRotationDegrees() {
checkState(outputRotationDegrees != C.LENGTH_UNSET);
return outputRotationDegrees;
} }
/** /**
@ -173,9 +131,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/ */
public boolean shouldProcess() { public boolean shouldProcess() {
checkStateNotNull(adjustedTransformationMatrix); checkStateNotNull(adjustedTransformationMatrix);
return inputWidth != outputWidth return !transformationMatrix.isIdentity();
|| inputHeight != outputHeight
|| !adjustedTransformationMatrix.isIdentity();
} }
@Override @Override
@ -184,11 +140,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.inputHeight = inputHeight; this.inputHeight = inputHeight;
adjustedTransformationMatrix = new Matrix(transformationMatrix); adjustedTransformationMatrix = new Matrix(transformationMatrix);
int displayWidth = inputWidth; if (transformationMatrix.isIdentity()) {
int displayHeight = inputHeight; return new Size(inputWidth, inputHeight);
if (!transformationMatrix.isIdentity()) { }
float inputAspectRatio = (float) inputWidth / inputHeight; float inputAspectRatio = (float) inputWidth / inputHeight;
// Scale frames by inputAspectRatio, to account for FrameProcessorChain's normalized device // Scale frames by inputAspectRatio, to account for OpenGL's normalized device
// coordinates (NDC) (a square from -1 to 1 for both x and y) and preserve rectangular // coordinates (NDC) (a square from -1 to 1 for both x and y) and preserve rectangular
// display of input pixels during transformations (ex. rotations). With scaling, // display of input pixels during transformations (ex. rotations). With scaling,
// transformationMatrix operations operate on a rectangle for x from -inputAspectRatio to // transformationMatrix operations operate on a rectangle for x from -inputAspectRatio to
@ -210,43 +167,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
yMax = max(yMax, transformOnNdcPoint[1]); yMax = max(yMax, transformOnNdcPoint[1]);
} }
float xCenter = (xMax + xMin) / 2f;
float yCenter = (yMax + yMin) / 2f;
adjustedTransformationMatrix.postTranslate(-xCenter, -yCenter);
float ndcWidthAndHeight = 2f; // Length from -1 to 1. float ndcWidthAndHeight = 2f; // Length from -1 to 1.
float xScale = (xMax - xMin) / ndcWidthAndHeight; float xScale = (xMax - xMin) / ndcWidthAndHeight;
float yScale = (yMax - yMin) / ndcWidthAndHeight; float yScale = (yMax - yMin) / ndcWidthAndHeight;
adjustedTransformationMatrix.postScale(1f / xScale, 1f / yScale); adjustedTransformationMatrix.postScale(1f / xScale, 1f / yScale);
displayWidth = Math.round(inputWidth * xScale);
displayHeight = Math.round(inputHeight * yScale);
}
// TODO(b/214975934): Move following requestedHeight and outputRotationDegrees logic into
// separate GlFrameProcessors (ex. Presentation).
// Scale width and height to desired requestedHeight, preserving aspect ratio.
if (requestedHeight != C.LENGTH_UNSET && requestedHeight != displayHeight) {
displayWidth = Math.round((float) requestedHeight * displayWidth / displayHeight);
displayHeight = requestedHeight;
}
// Encoders commonly support higher maximum widths than maximum heights. Rotate the decoded
// video before encoding, so the encoded video's width >= height, and set
// outputRotationDegrees to ensure the video is displayed in the correct orientation.
if (displayHeight > displayWidth) {
outputRotationDegrees = 90;
outputWidth = displayHeight;
outputHeight = displayWidth;
// TODO(b/201293185): After fragment shader transformations are implemented, put
// postRotate in a later GlFrameProcessor.
adjustedTransformationMatrix.postRotate(outputRotationDegrees);
} else {
outputRotationDegrees = 0;
outputWidth = displayWidth;
outputHeight = displayHeight;
}
int outputWidth = Math.round(inputWidth * xScale);
int outputHeight = Math.round(inputHeight * yScale);
return new Size(outputWidth, outputHeight); return new Size(outputWidth, outputHeight);
} }

View File

@ -140,7 +140,6 @@ public final class TransformationRequest {
* @return This builder. * @return This builder.
*/ */
public Builder setResolution(int outputHeight) { public Builder setResolution(int outputHeight) {
// TODO(b/201293185): Restructure to input a Presentation class.
this.outputHeight = outputHeight; this.outputHeight = outputHeight;
return this; return this;
} }

View File

@ -74,15 +74,19 @@ import org.checkerframework.dataflow.qual.Pure;
new ScaleToFitFrameProcessor.Builder(context) new ScaleToFitFrameProcessor.Builder(context)
.setScale(transformationRequest.scaleX, transformationRequest.scaleY) .setScale(transformationRequest.scaleX, transformationRequest.scaleY)
.setRotationDegrees(transformationRequest.rotationDegrees) .setRotationDegrees(transformationRequest.rotationDegrees)
.build();
PresentationFrameProcessor presentationFrameProcessor =
new PresentationFrameProcessor.Builder(context)
.setResolution(transformationRequest.outputHeight) .setResolution(transformationRequest.outputHeight)
.build(); .build();
// TODO(b/214975934): Allow a list of frame processors to be passed into the sample pipeline. // TODO(b/214975934): Allow a list of frame processors to be passed into the sample pipeline.
ImmutableList<GlFrameProcessor> frameProcessors = ImmutableList.of(scaleToFitFrameProcessor); ImmutableList<GlFrameProcessor> frameProcessors =
ImmutableList.of(scaleToFitFrameProcessor, presentationFrameProcessor);
List<Size> frameProcessorSizes = List<Size> frameProcessorSizes =
FrameProcessorChain.configureSizes(decodedWidth, decodedHeight, frameProcessors); FrameProcessorChain.configureSizes(decodedWidth, decodedHeight, frameProcessors);
Size requestedEncoderSize = Iterables.getLast(frameProcessorSizes); Size requestedEncoderSize = Iterables.getLast(frameProcessorSizes);
// TODO(b/213190310): Move output rotation configuration to PresentationFrameProcessor. // TODO(b/213190310): Move output rotation configuration to PresentationFrameProcessor.
outputRotationDegrees = scaleToFitFrameProcessor.getOutputRotationDegrees(); outputRotationDegrees = presentationFrameProcessor.getOutputRotationDegrees();
Format requestedEncoderFormat = Format requestedEncoderFormat =
new Format.Builder() new Format.Builder()
@ -109,6 +113,7 @@ import org.checkerframework.dataflow.qual.Pure;
|| inputFormat.height != encoderSupportedFormat.height || inputFormat.height != encoderSupportedFormat.height
|| inputFormat.width != encoderSupportedFormat.width || inputFormat.width != encoderSupportedFormat.width
|| scaleToFitFrameProcessor.shouldProcess() || scaleToFitFrameProcessor.shouldProcess()
|| presentationFrameProcessor.shouldProcess()
|| shouldAlwaysUseFrameProcessorChain()) { || shouldAlwaysUseFrameProcessorChain()) {
// TODO(b/218488308): Allow the final GlFrameProcessor to be re-configured if its output size // TODO(b/218488308): Allow the final GlFrameProcessor to be re-configured if its output size
// has to change due to encoder fallback or append another GlFrameProcessor. // has to change due to encoder fallback or append another GlFrameProcessor.

View File

@ -0,0 +1,106 @@
/*
* Copyright 2022 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.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.util.Size;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Unit tests for {@link PresentationFrameProcessor}.
*
* <p>See {@code AdvancedFrameProcessorPixelTest} for pixel tests testing {@link
* AdvancedFrameProcessor} given a transformation matrix.
*/
@RunWith(AndroidJUnit4.class)
public final class PresentationFrameProcessorTest {
@Test
public void configureOutputSize_noEditsLandscape_leavesFramesUnchanged() {
int inputWidth = 200;
int inputHeight = 150;
PresentationFrameProcessor presentationFrameProcessor =
new PresentationFrameProcessor.Builder(getApplicationContext()).build();
Size outputSize = presentationFrameProcessor.configureOutputSize(inputWidth, inputHeight);
assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(0);
assertThat(presentationFrameProcessor.shouldProcess()).isFalse();
assertThat(outputSize.getWidth()).isEqualTo(inputWidth);
assertThat(outputSize.getHeight()).isEqualTo(inputHeight);
}
@Test
public void configureOutputSize_noEditsSquare_leavesFramesUnchanged() {
int inputWidth = 150;
int inputHeight = 150;
PresentationFrameProcessor presentationFrameProcessor =
new PresentationFrameProcessor.Builder(getApplicationContext()).build();
Size outputSize = presentationFrameProcessor.configureOutputSize(inputWidth, inputHeight);
assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(0);
assertThat(presentationFrameProcessor.shouldProcess()).isFalse();
assertThat(outputSize.getWidth()).isEqualTo(inputWidth);
assertThat(outputSize.getHeight()).isEqualTo(inputHeight);
}
@Test
public void configureOutputSize_noEditsPortrait_flipsOrientation() {
int inputWidth = 150;
int inputHeight = 200;
PresentationFrameProcessor presentationFrameProcessor =
new PresentationFrameProcessor.Builder(getApplicationContext()).build();
Size outputSize = presentationFrameProcessor.configureOutputSize(inputWidth, inputHeight);
assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(90);
assertThat(presentationFrameProcessor.shouldProcess()).isTrue();
assertThat(outputSize.getWidth()).isEqualTo(inputHeight);
assertThat(outputSize.getHeight()).isEqualTo(inputWidth);
}
@Test
public void configureOutputSize_setResolution_changesDimensions() {
int inputWidth = 200;
int inputHeight = 150;
int requestedHeight = 300;
PresentationFrameProcessor presentationFrameProcessor =
new PresentationFrameProcessor.Builder(getApplicationContext())
.setResolution(requestedHeight)
.build();
Size outputSize = presentationFrameProcessor.configureOutputSize(inputWidth, inputHeight);
assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(0);
assertThat(presentationFrameProcessor.shouldProcess()).isTrue();
assertThat(outputSize.getWidth()).isEqualTo(requestedHeight * inputWidth / inputHeight);
assertThat(outputSize.getHeight()).isEqualTo(requestedHeight);
}
@Test
public void getOutputRotationDegreesBeforeConfigure_throwsIllegalStateException() {
PresentationFrameProcessor presentationFrameProcessor =
new PresentationFrameProcessor.Builder(getApplicationContext()).build();
// configureOutputSize not called before initialize.
assertThrows(IllegalStateException.class, presentationFrameProcessor::getOutputRotationDegrees);
}
}

View File

@ -34,7 +34,7 @@ import org.junit.runner.RunWith;
public final class ScaleToFitFrameProcessorTest { public final class ScaleToFitFrameProcessorTest {
@Test @Test
public void configureOutputDimensions_noEdits_producesExpectedOutput() { public void configureOutputSize_noEdits_leavesFramesUnchanged() {
int inputWidth = 200; int inputWidth = 200;
int inputHeight = 150; int inputHeight = 150;
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
@ -42,7 +42,6 @@ public final class ScaleToFitFrameProcessorTest {
Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight);
assertThat(scaleToFitFrameProcessor.getOutputRotationDegrees()).isEqualTo(0);
assertThat(scaleToFitFrameProcessor.shouldProcess()).isFalse(); assertThat(scaleToFitFrameProcessor.shouldProcess()).isFalse();
assertThat(outputSize.getWidth()).isEqualTo(inputWidth); assertThat(outputSize.getWidth()).isEqualTo(inputWidth);
assertThat(outputSize.getHeight()).isEqualTo(inputHeight); assertThat(outputSize.getHeight()).isEqualTo(inputHeight);
@ -53,23 +52,14 @@ public final class ScaleToFitFrameProcessorTest {
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
new ScaleToFitFrameProcessor.Builder(getApplicationContext()).build(); new ScaleToFitFrameProcessor.Builder(getApplicationContext()).build();
// configureOutputDimensions not called before initialize. // configureOutputSize not called before initialize.
assertThrows( assertThrows(
IllegalStateException.class, IllegalStateException.class,
() -> scaleToFitFrameProcessor.initialize(/* inputTexId= */ 0)); () -> scaleToFitFrameProcessor.initialize(/* inputTexId= */ 0));
} }
@Test @Test
public void getOutputRotationDegreesBeforeConfigure_throwsIllegalStateException() { public void configureOutputSize_scaleNarrow_decreasesWidth() {
ScaleToFitFrameProcessor scaleToFitFrameProcessor =
new ScaleToFitFrameProcessor.Builder(getApplicationContext()).build();
// configureOutputDimensions not called before initialize.
assertThrows(IllegalStateException.class, scaleToFitFrameProcessor::getOutputRotationDegrees);
}
@Test
public void configureOutputDimensions_scaleNarrow_producesExpectedOutput() {
int inputWidth = 200; int inputWidth = 200;
int inputHeight = 150; int inputHeight = 150;
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
@ -79,14 +69,13 @@ public final class ScaleToFitFrameProcessorTest {
Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight);
assertThat(scaleToFitFrameProcessor.getOutputRotationDegrees()).isEqualTo(90);
assertThat(scaleToFitFrameProcessor.shouldProcess()).isTrue(); assertThat(scaleToFitFrameProcessor.shouldProcess()).isTrue();
assertThat(outputSize.getWidth()).isEqualTo(inputHeight); assertThat(outputSize.getWidth()).isEqualTo(Math.round(inputWidth * .5f));
assertThat(outputSize.getHeight()).isEqualTo(Math.round(inputWidth * .5f)); assertThat(outputSize.getHeight()).isEqualTo(inputHeight);
} }
@Test @Test
public void configureOutputDimensions_scaleWide_producesExpectedOutput() { public void configureOutputSize_scaleWide_increasesWidth() {
int inputWidth = 200; int inputWidth = 200;
int inputHeight = 150; int inputHeight = 150;
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
@ -96,14 +85,13 @@ public final class ScaleToFitFrameProcessorTest {
Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight);
assertThat(scaleToFitFrameProcessor.getOutputRotationDegrees()).isEqualTo(0);
assertThat(scaleToFitFrameProcessor.shouldProcess()).isTrue(); assertThat(scaleToFitFrameProcessor.shouldProcess()).isTrue();
assertThat(outputSize.getWidth()).isEqualTo(inputWidth * 2); assertThat(outputSize.getWidth()).isEqualTo(inputWidth * 2);
assertThat(outputSize.getHeight()).isEqualTo(inputHeight); assertThat(outputSize.getHeight()).isEqualTo(inputHeight);
} }
@Test @Test
public void configureOutputDimensions_scaleTall_producesExpectedOutput() { public void configureOutputDimensions_scaleTall_increasesHeight() {
int inputWidth = 200; int inputWidth = 200;
int inputHeight = 150; int inputHeight = 150;
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
@ -113,14 +101,13 @@ public final class ScaleToFitFrameProcessorTest {
Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight);
assertThat(scaleToFitFrameProcessor.getOutputRotationDegrees()).isEqualTo(90);
assertThat(scaleToFitFrameProcessor.shouldProcess()).isTrue(); assertThat(scaleToFitFrameProcessor.shouldProcess()).isTrue();
assertThat(outputSize.getWidth()).isEqualTo(inputHeight * 2); assertThat(outputSize.getWidth()).isEqualTo(inputWidth);
assertThat(outputSize.getHeight()).isEqualTo(inputWidth); assertThat(outputSize.getHeight()).isEqualTo(inputHeight * 2);
} }
@Test @Test
public void configureOutputDimensions_rotate90_producesExpectedOutput() { public void configureOutputSize_rotate90_swapsDimensions() {
int inputWidth = 200; int inputWidth = 200;
int inputHeight = 150; int inputHeight = 150;
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
@ -130,14 +117,13 @@ public final class ScaleToFitFrameProcessorTest {
Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight);
assertThat(scaleToFitFrameProcessor.getOutputRotationDegrees()).isEqualTo(90);
assertThat(scaleToFitFrameProcessor.shouldProcess()).isTrue(); assertThat(scaleToFitFrameProcessor.shouldProcess()).isTrue();
assertThat(outputSize.getWidth()).isEqualTo(inputWidth); assertThat(outputSize.getWidth()).isEqualTo(inputHeight);
assertThat(outputSize.getHeight()).isEqualTo(inputHeight); assertThat(outputSize.getHeight()).isEqualTo(inputWidth);
} }
@Test @Test
public void configureOutputDimensions_rotate45_producesExpectedOutput() { public void configureOutputSize_rotate45_changesDimensions() {
int inputWidth = 200; int inputWidth = 200;
int inputHeight = 150; int inputHeight = 150;
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
@ -148,27 +134,8 @@ public final class ScaleToFitFrameProcessorTest {
Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight);
assertThat(scaleToFitFrameProcessor.getOutputRotationDegrees()).isEqualTo(0);
assertThat(scaleToFitFrameProcessor.shouldProcess()).isTrue(); assertThat(scaleToFitFrameProcessor.shouldProcess()).isTrue();
assertThat(outputSize.getWidth()).isEqualTo(expectedOutputWidthHeight); assertThat(outputSize.getWidth()).isEqualTo(expectedOutputWidthHeight);
assertThat(outputSize.getHeight()).isEqualTo(expectedOutputWidthHeight); assertThat(outputSize.getHeight()).isEqualTo(expectedOutputWidthHeight);
} }
@Test
public void configureOutputDimensions_setResolution_producesExpectedOutput() {
int inputWidth = 200;
int inputHeight = 150;
int requestedHeight = 300;
ScaleToFitFrameProcessor scaleToFitFrameProcessor =
new ScaleToFitFrameProcessor.Builder(getApplicationContext())
.setResolution(requestedHeight)
.build();
Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight);
assertThat(scaleToFitFrameProcessor.getOutputRotationDegrees()).isEqualTo(0);
assertThat(scaleToFitFrameProcessor.shouldProcess()).isTrue();
assertThat(outputSize.getWidth()).isEqualTo(requestedHeight * inputWidth / inputHeight);
assertThat(outputSize.getHeight()).isEqualTo(requestedHeight);
}
} }