Transformer GL: Split Presentation and Crop.

This removes the prior restriction of needing to remember not to crop and set aspect ratio in the same Presentation.Builder, and makes each class a bit more targeted.

This is partially made feasible by the past work to merge consecutive
MatrixTransformations into a single MatrixTransformationFrameProcessor, which
ensures that there's no loss in quality between successive MatrixTransformations.

PiperOrigin-RevId: 453660582
This commit is contained in:
huangdarwin 2022-06-08 12:51:28 +00:00 committed by Marc Baechinger
parent b3b57bc93d
commit b33dc5e57b
8 changed files with 366 additions and 254 deletions

View File

@ -0,0 +1,170 @@
/*
* 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.transformer;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.android.exoplayer2.transformer.BitmapTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.graphics.Bitmap;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLSurface;
import android.util.Size;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.util.GlUtil;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Pixel test for texture processing via {@link Crop}.
*
* <p>Expected images are taken from an emulator, so tests on different emulators or physical
* devices may fail. To test on other devices, please increase the {@link
* BitmapTestUtil#MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE} and/or inspect the saved output bitmaps
* as recommended in {@link FrameProcessorChainPixelTest}.
*/
@RunWith(AndroidJUnit4.class)
public final class CropPixelTest {
public static final String ORIGINAL_PNG_ASSET_PATH =
"media/bitmap/sample_mp4_first_frame/original.png";
public static final String CROP_SMALLER_PNG_ASSET_PATH =
"media/bitmap/sample_mp4_first_frame/crop_smaller.png";
public static final String CROP_LARGER_PNG_ASSET_PATH =
"media/bitmap/sample_mp4_first_frame/crop_larger.png";
static {
GlUtil.glAssertionsEnabled = true;
}
private final Context context = getApplicationContext();
private final EGLDisplay eglDisplay = GlUtil.createEglDisplay();
private final EGLContext eglContext = GlUtil.createEglContext(eglDisplay);
private @MonotonicNonNull SingleFrameGlTextureProcessor cropTextureProcessor;
private @MonotonicNonNull EGLSurface placeholderEglSurface;
private int inputTexId;
private int outputTexId;
private int inputWidth;
private int inputHeight;
@Before
public void createTextures() throws IOException {
Bitmap inputBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH);
inputWidth = inputBitmap.getWidth();
inputHeight = inputBitmap.getHeight();
placeholderEglSurface = GlUtil.createPlaceholderEglSurface(eglDisplay);
GlUtil.focusEglSurface(eglDisplay, eglContext, placeholderEglSurface, inputWidth, inputHeight);
inputTexId = BitmapTestUtil.createGlTextureFromBitmap(inputBitmap);
}
@After
public void release() {
if (cropTextureProcessor != null) {
cropTextureProcessor.release();
}
GlUtil.destroyEglContext(eglDisplay, eglContext);
}
@Test
public void drawFrame_noEdits_producesExpectedOutput() throws Exception {
String testId = "drawFrame_noEdits";
cropTextureProcessor =
new Crop(/* left= */ -1, /* right= */ 1, /* bottom= */ -1, /* top= */ 1)
.toGlTextureProcessor(context);
Size outputSize = cropTextureProcessor.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH);
cropTextureProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
outputSize.getWidth(), outputSize.getHeight());
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
testId, /* bitmapLabel= */ "actual", actualBitmap);
// TODO(b/207848601): switch to using proper tooling for testing against golden data.
float averagePixelAbsoluteDifference =
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
expectedBitmap, actualBitmap, testId);
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void drawFrame_cropSmaller_producesExpectedOutput() throws Exception {
String testId = "drawFrame_cropSmaller";
cropTextureProcessor =
new Crop(/* left= */ -.9f, /* right= */ .1f, /* bottom= */ -1f, /* top= */ .5f)
.toGlTextureProcessor(context);
Size outputSize = cropTextureProcessor.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(CROP_SMALLER_PNG_ASSET_PATH);
cropTextureProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
outputSize.getWidth(), outputSize.getHeight());
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
testId, /* bitmapLabel= */ "actual", actualBitmap);
// TODO(b/207848601): switch to using proper tooling for testing against golden data.
float averagePixelAbsoluteDifference =
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
expectedBitmap, actualBitmap, testId);
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void drawFrame_cropLarger_producesExpectedOutput() throws Exception {
String testId = "drawFrame_cropLarger";
cropTextureProcessor =
new Crop(/* left= */ -2f, /* right= */ 2f, /* bottom= */ -1f, /* top= */ 2f)
.toGlTextureProcessor(context);
Size outputSize = cropTextureProcessor.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(CROP_LARGER_PNG_ASSET_PATH);
cropTextureProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
outputSize.getWidth(), outputSize.getHeight());
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
testId, /* bitmapLabel= */ "actual", actualBitmap);
// TODO(b/207848601): switch to using proper tooling for testing against golden data.
float averagePixelAbsoluteDifference =
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
expectedBitmap, actualBitmap, testId);
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
private void setupOutputTexture(int outputWidth, int outputHeight) {
outputTexId = GlUtil.createTexture(outputWidth, outputHeight);
int frameBuffer = GlUtil.createFboForTexture(outputTexId);
GlUtil.focusFramebuffer(
eglDisplay,
eglContext,
checkNotNull(placeholderEglSurface),
frameBuffer,
outputWidth,
outputHeight);
}
}

View File

@ -67,6 +67,8 @@ public final class FrameProcessorChainPixelTest {
"media/bitmap/sample_mp4_first_frame/translate_then_rotate.png";
public static final String REQUEST_OUTPUT_HEIGHT_PNG_ASSET_PATH =
"media/bitmap/sample_mp4_first_frame/request_output_height.png";
public static final String CROP_THEN_ASPECT_RATIO_PNG_ASSET_PATH =
"media/bitmap/sample_mp4_first_frame/crop_then_aspect_ratio.png";
public static final String ROTATE45_SCALE_TO_FIT_PNG_ASSET_PATH =
"media/bitmap/sample_mp4_first_frame/rotate_45_scale_to_fit.png";
@ -216,6 +218,28 @@ public final class FrameProcessorChainPixelTest {
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void processData_withCropAndPresentation_producesExpectedOutput() throws Exception {
String testId = "processData_withCropAndPresentation";
setUpAndPrepareFirstFrame(
DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO,
new Crop(/* left= */ -.5f, /* right= */ .5f, /* bottom= */ -.5f, /* top= */ .5f),
new Presentation.Builder()
.setAspectRatio(/* aspectRatio= */ .5f, Presentation.LAYOUT_SCALE_TO_FIT)
.build());
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(CROP_THEN_ASPECT_RATIO_PNG_ASSET_PATH);
Bitmap actualBitmap = processFirstFrameAndEnd();
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
testId, /* bitmapLabel= */ "actual", actualBitmap);
// TODO(b/207848601): switch to using proper tooling for testing against golden data.
float averagePixelAbsoluteDifference =
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
expectedBitmap, actualBitmap, testId);
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void processData_withScaleToFitTransformation_rotate45_producesExpectedOutput()
throws Exception {
@ -242,10 +266,8 @@ public final class FrameProcessorChainPixelTest {
throws Exception {
String testId =
"processData_withManyComposedMatrixTransformations_producesSameOutputAsCombinedTransformation";
Presentation centerCrop =
new Presentation.Builder()
.setCrop(/* left= */ -0.5f, /* right= */ 0.5f, /* bottom= */ -0.5f, /* top= */ 0.5f)
.build();
Crop centerCrop =
new Crop(/* left= */ -0.5f, /* right= */ 0.5f, /* bottom= */ -0.5f, /* top= */ 0.5f);
ImmutableList.Builder<GlEffect> full10StepRotationAndCenterCrop = new ImmutableList.Builder<>();
for (int i = 0; i < 10; i++) {
full10StepRotationAndCenterCrop.add(new Rotation(/* degrees= */ 36));

View File

@ -47,10 +47,6 @@ import org.junit.runner.RunWith;
public final class PresentationPixelTest {
public static final String ORIGINAL_PNG_ASSET_PATH =
"media/bitmap/sample_mp4_first_frame/original.png";
public static final String CROP_SMALLER_PNG_ASSET_PATH =
"media/bitmap/sample_mp4_first_frame/crop_smaller.png";
public static final String CROP_LARGER_PNG_ASSET_PATH =
"media/bitmap/sample_mp4_first_frame/crop_larger.png";
public static final String ASPECT_RATIO_SCALE_TO_FIT_NARROW_PNG_ASSET_PATH =
"media/bitmap/sample_mp4_first_frame/aspect_ratio_scale_to_fit_narrow.png";
public static final String ASPECT_RATIO_SCALE_TO_FIT_WIDE_PNG_ASSET_PATH =
@ -118,58 +114,6 @@ public final class PresentationPixelTest {
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void drawFrame_cropSmaller_producesExpectedOutput() throws Exception {
String testId = "drawFrame_cropSmaller";
presentationTextureProcessor =
new Presentation.Builder()
.setCrop(/* left= */ -.9f, /* right= */ .1f, /* bottom= */ -1f, /* top= */ .5f)
.build()
.toGlTextureProcessor(context);
Size outputSize = presentationTextureProcessor.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(CROP_SMALLER_PNG_ASSET_PATH);
presentationTextureProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
outputSize.getWidth(), outputSize.getHeight());
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
testId, /* bitmapLabel= */ "actual", actualBitmap);
// TODO(b/207848601): switch to using proper tooling for testing against golden data.
float averagePixelAbsoluteDifference =
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
expectedBitmap, actualBitmap, testId);
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void drawFrame_cropLarger_producesExpectedOutput() throws Exception {
String testId = "drawFrame_cropSmaller";
presentationTextureProcessor =
new Presentation.Builder()
.setCrop(/* left= */ -2f, /* right= */ 2f, /* bottom= */ -1f, /* top= */ 2f)
.build()
.toGlTextureProcessor(context);
Size outputSize = presentationTextureProcessor.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(CROP_LARGER_PNG_ASSET_PATH);
presentationTextureProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
outputSize.getWidth(), outputSize.getHeight());
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
testId, /* bitmapLabel= */ "actual", actualBitmap);
// TODO(b/207848601): switch to using proper tooling for testing against golden data.
float averagePixelAbsoluteDifference =
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
expectedBitmap, actualBitmap, testId);
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void drawFrame_changeAspectRatio_scaleToFit_narrow_producesExpectedOutput()
throws Exception {

View File

@ -0,0 +1,98 @@
/*
* 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 com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import android.graphics.Matrix;
import android.util.Size;
import com.google.android.exoplayer2.util.GlUtil;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Specifies a crop to apply in the vertex shader.
*
* <p>The background color of the output frame will be black, with alpha = 0 if applicable.
*/
public final class Crop implements MatrixTransformation {
static {
GlUtil.glAssertionsEnabled = true;
}
private final float left;
private final float right;
private final float bottom;
private final float top;
private @MonotonicNonNull Matrix transformationMatrix;
/**
* Crops a smaller (or larger) frame, per normalized device coordinates (NDC), where the input
* frame corresponds to the square ranging from -1 to 1 on the x and y axes.
*
* <p>{@code left} and {@code bottom} default to -1, and {@code right} and {@code top} default to
* 1, which corresponds to not applying any crop. To crop to a smaller subset of the input frame,
* use values between -1 and 1. To crop to a larger frame, use values below -1 and above 1.
*
* @param left The left edge of the output frame, in NDC. Must be less than {@code right}.
* @param right The right edge of the output frame, in NDC. Must be greater than {@code left}.
* @param bottom The bottom edge of the output frame, in NDC. Must be less than {@code top}.
* @param top The top edge of the output frame, in NDC. Must be greater than {@code bottom}.
*/
public Crop(float left, float right, float bottom, float top) {
checkArgument(
right > left, "right value " + right + " should be greater than left value " + left);
checkArgument(
top > bottom, "top value " + top + " should be greater than bottom value " + bottom);
this.left = left;
this.right = right;
this.bottom = bottom;
this.top = top;
transformationMatrix = new Matrix();
}
@Override
public Size configure(int inputWidth, int inputHeight) {
checkArgument(inputWidth > 0, "inputWidth must be positive");
checkArgument(inputHeight > 0, "inputHeight must be positive");
transformationMatrix = new Matrix();
if (left == -1f && right == 1f && bottom == -1f && top == 1f) {
// No crop needed.
return new Size(inputWidth, inputHeight);
}
float scaleX = (right - left) / GlUtil.LENGTH_NDC;
float scaleY = (top - bottom) / GlUtil.LENGTH_NDC;
float centerX = (left + right) / 2;
float centerY = (bottom + top) / 2;
transformationMatrix.postTranslate(-centerX, -centerY);
transformationMatrix.postScale(1f / scaleX, 1f / scaleY);
int outputWidth = Math.round(inputWidth * scaleX);
int outputHeight = Math.round(inputHeight * scaleY);
return new Size(outputWidth, outputHeight);
}
@Override
public Matrix getMatrix(long presentationTimeUs) {
return checkStateNotNull(transformationMatrix, "configure must be called first");
}
}

View File

@ -16,7 +16,6 @@
package com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.SOURCE;
@ -33,11 +32,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* Controls how a frame is presented with options to set the output resolution, crop the input, and
* choose how to map the input pixels onto the output frame geometry (for example, by stretching the
* input frame to match the specified output frame, or fitting the input frame using letterboxing).
* Controls how a frame is presented with options to set the output resolution and choose how to map
* the input pixels onto the output frame geometry (for example, by stretching the input frame to
* match the specified output frame, or fitting the input frame using letterboxing).
*
* <p>Cropping or aspect ratio is applied before setting resolution.
* <p>Aspect ratio is applied before setting resolution.
*
* <p>The background color of the output frame will be black, with alpha = 0 if applicable.
*/
@ -104,21 +103,13 @@ public final class Presentation implements MatrixTransformation {
public static final class Builder {
// Optional fields.
private int heightPixels;
private float cropLeft;
private float cropRight;
private float cropBottom;
private float cropTop;
private int outputHeight;
private float aspectRatio;
private @Layout int layout;
/** Creates a builder with default values. */
public Builder() {
heightPixels = C.LENGTH_UNSET;
cropLeft = -1f;
cropRight = 1f;
cropBottom = -1f;
cropTop = 1f;
outputHeight = C.LENGTH_UNSET;
aspectRatio = C.LENGTH_UNSET;
}
@ -136,44 +127,7 @@ public final class Presentation implements MatrixTransformation {
* @return This builder.
*/
public Builder setResolution(int height) {
this.heightPixels = height;
return this;
}
/**
* Crops a smaller (or larger frame), per normalized device coordinates (NDC), where the input
* frame corresponds to the square ranging from -1 to 1 on the x and y axes.
*
* <p>{@code left} and {@code bottom} default to -1, and {@code right} and {@code top} default
* to 1, which corresponds to not applying any crop. To crop to a smaller subset of the input
* frame, use values between -1 and 1. To crop to a larger frame, use values below -1 and above
* 1.
*
* <p>Width and height values set may be rescaled by {@link #setResolution(int)}, which is
* applied after cropping changes.
*
* <p>Only one of {@code setCrop} or {@link #setAspectRatio(float, int)} can be called for one
* {@link Presentation}.
*
* @param left The left edge of the output frame, in NDC. Must be less than {@code right}.
* @param right The right edge of the output frame, in NDC. Must be greater than {@code left}.
* @param bottom The bottom edge of the output frame, in NDC. Must be less than {@code top}.
* @param top The top edge of the output frame, in NDC. Must be greater than {@code bottom}.
* @return This builder.
*/
public Builder setCrop(float left, float right, float bottom, float top) {
checkArgument(
right > left, "right value " + right + " should be greater than left value " + left);
checkArgument(
top > bottom, "top value " + top + " should be greater than bottom value " + bottom);
checkState(
aspectRatio == C.LENGTH_UNSET,
"setAspectRatio and setCrop cannot be called in the same instance");
cropLeft = left;
cropRight = right;
cropBottom = bottom;
cropTop = top;
this.outputHeight = height;
return this;
}
@ -187,9 +141,6 @@ public final class Presentation implements MatrixTransformation {
* <p>Width and height values set may be rescaled by {@link #setResolution(int)}, which is
* applied after aspect ratio changes.
*
* <p>Only one of {@link #setCrop(float, float, float, float)} or {@code setAspectRatio} can be
* called for one {@link Presentation}.
*
* @param aspectRatio The aspect ratio (width/height ratio) of the output frame. Must be
* positive.
* @return This builder.
@ -201,17 +152,13 @@ public final class Presentation implements MatrixTransformation {
|| layout == LAYOUT_SCALE_TO_FIT_WITH_CROP
|| layout == LAYOUT_STRETCH_TO_FIT,
"invalid layout " + layout);
checkState(
cropLeft == -1f && cropRight == 1f && cropBottom == -1f && cropTop == 1f,
"setAspectRatio and setCrop cannot be called in the same instance");
this.aspectRatio = aspectRatio;
this.layout = layout;
return this;
}
public Presentation build() {
return new Presentation(
heightPixels, cropLeft, cropRight, cropBottom, cropTop, aspectRatio, layout);
return new Presentation(outputHeight, aspectRatio, layout);
}
}
@ -220,10 +167,6 @@ public final class Presentation implements MatrixTransformation {
}
private final int requestedHeightPixels;
private final float cropLeft;
private final float cropRight;
private final float cropBottom;
private final float cropTop;
private final float requestedAspectRatio;
private final @Layout int layout;
@ -232,19 +175,8 @@ public final class Presentation implements MatrixTransformation {
private @MonotonicNonNull Matrix transformationMatrix;
/** Creates a new instance. */
private Presentation(
int requestedHeightPixels,
float cropLeft,
float cropRight,
float cropBottom,
float cropTop,
float requestedAspectRatio,
@Layout int layout) {
private Presentation(int requestedHeightPixels, float requestedAspectRatio, @Layout int layout) {
this.requestedHeightPixels = requestedHeightPixels;
this.cropLeft = cropLeft;
this.cropRight = cropRight;
this.cropBottom = cropBottom;
this.cropTop = cropTop;
this.requestedAspectRatio = requestedAspectRatio;
this.layout = layout;
@ -262,12 +194,7 @@ public final class Presentation implements MatrixTransformation {
outputWidth = inputWidth;
outputHeight = inputHeight;
if (cropLeft != -1f || cropRight != 1f || cropBottom != -1f || cropTop != 1f) {
checkState(
requestedAspectRatio == C.LENGTH_UNSET,
"aspect ratio and crop cannot both be set in the same instance");
applyCrop();
} else if (requestedAspectRatio != C.LENGTH_UNSET) {
if (requestedAspectRatio != C.LENGTH_UNSET) {
applyAspectRatio();
}
@ -284,20 +211,6 @@ public final class Presentation implements MatrixTransformation {
return checkStateNotNull(transformationMatrix, "configure must be called first");
}
@RequiresNonNull("transformationMatrix")
private void applyCrop() {
float scaleX = (cropRight - cropLeft) / GlUtil.LENGTH_NDC;
float scaleY = (cropTop - cropBottom) / GlUtil.LENGTH_NDC;
float centerX = (cropLeft + cropRight) / 2;
float centerY = (cropBottom + cropTop) / 2;
transformationMatrix.postTranslate(-centerX, -centerY);
transformationMatrix.postScale(1f / scaleX, 1f / scaleY);
outputWidth = outputWidth * scaleX;
outputHeight = outputHeight * scaleY;
}
@RequiresNonNull("transformationMatrix")
private void applyAspectRatio() {
float inputAspectRatio = outputWidth / outputHeight;

View File

@ -0,0 +1,62 @@
/*
* 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 com.google.android.exoplayer2.transformer;
import static com.google.common.truth.Truth.assertThat;
import android.util.Size;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.util.GlUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Unit tests for {@link Crop}.
*
* <p>See {@code CropPixelTest} for pixel tests testing {@link Crop}.
*/
@RunWith(AndroidJUnit4.class)
public final class CropTest {
@Test
public void configure_noEdits_leavesFramesUnchanged() {
int inputWidth = 200;
int inputHeight = 150;
Crop crop = new Crop(/* left= */ -1, /* right= */ 1, /* bottom= */ -1, /* top= */ 1);
Size outputSize = crop.configure(inputWidth, inputHeight);
assertThat(outputSize.getWidth()).isEqualTo(inputWidth);
assertThat(outputSize.getHeight()).isEqualTo(inputHeight);
}
@Test
public void configure_setCrop_changesDimensions() {
int inputWidth = 300;
int inputHeight = 200;
float left = -.5f;
float right = .5f;
float bottom = .5f;
float top = 1f;
Crop crop = new Crop(left, right, bottom, top);
Size outputSize = crop.configure(inputWidth, inputHeight);
int expectedPostCropWidth = Math.round(inputWidth * (right - left) / GlUtil.LENGTH_NDC);
int expectedPostCropHeight = Math.round(inputHeight * (top - bottom) / GlUtil.LENGTH_NDC);
assertThat(outputSize.getWidth()).isEqualTo(expectedPostCropWidth);
assertThat(outputSize.getHeight()).isEqualTo(expectedPostCropHeight);
}
}

View File

@ -16,11 +16,9 @@
package com.google.android.exoplayer2.transformer;
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 com.google.android.exoplayer2.util.GlUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -56,75 +54,6 @@ public final class PresentationTest {
assertThat(outputSize.getHeight()).isEqualTo(requestedHeight);
}
@Test
public void configure_setCrop_changesDimensions() {
int inputWidth = 300;
int inputHeight = 200;
float left = -.5f;
float right = .5f;
float bottom = .5f;
float top = 1f;
Presentation presentation =
new Presentation.Builder().setCrop(left, right, bottom, top).build();
Size outputSize = presentation.configure(inputWidth, inputHeight);
int expectedPostCropWidth = Math.round(inputWidth * (right - left) / GlUtil.LENGTH_NDC);
int expectedPostCropHeight = Math.round(inputHeight * (top - bottom) / GlUtil.LENGTH_NDC);
assertThat(outputSize.getWidth()).isEqualTo(expectedPostCropWidth);
assertThat(outputSize.getHeight()).isEqualTo(expectedPostCropHeight);
}
@Test
public void configure_setCropAndSetResolution_changesDimensions() {
int inputWidth = 300;
int inputHeight = 200;
float left = -.5f;
float right = .5f;
float bottom = .5f;
float top = 1f;
int requestedHeight = 100;
Presentation presentation =
new Presentation.Builder()
.setCrop(left, right, bottom, top)
.setResolution(requestedHeight)
.build();
Size outputSize = presentation.configure(inputWidth, inputHeight);
int expectedPostCropWidth = Math.round(inputWidth * (right - left) / GlUtil.LENGTH_NDC);
int expectedPostCropHeight = Math.round(inputHeight * (top - bottom) / GlUtil.LENGTH_NDC);
assertThat(outputSize.getWidth())
.isEqualTo(
Math.round((float) requestedHeight * expectedPostCropWidth / expectedPostCropHeight));
assertThat(outputSize.getHeight()).isEqualTo(requestedHeight);
}
@Test
public void configure_setResolutionAndCrop_changesDimensions() {
int inputWidth = 300;
int inputHeight = 200;
float left = -.5f;
float right = .5f;
float bottom = .5f;
float top = 1f;
int requestedHeight = 100;
Presentation presentation =
new Presentation.Builder()
.setResolution(requestedHeight)
.setCrop(left, right, bottom, top)
.build();
Size outputSize = presentation.configure(inputWidth, inputHeight);
int expectedPostCropWidth = Math.round(inputWidth * (right - left) / GlUtil.LENGTH_NDC);
int expectedPostCropHeight = Math.round(inputHeight * (top - bottom) / GlUtil.LENGTH_NDC);
assertThat(outputSize.getWidth())
.isEqualTo(
Math.round((float) requestedHeight * expectedPostCropWidth / expectedPostCropHeight));
assertThat(outputSize.getHeight()).isEqualTo(requestedHeight);
}
@Test
public void configure_setAspectRatio_changesDimensions() {
int inputWidth = 300;
@ -158,30 +87,4 @@ public final class PresentationTest {
assertThat(outputSize.getWidth()).isEqualTo(Math.round(aspectRatio * requestedHeight));
assertThat(outputSize.getHeight()).isEqualTo(requestedHeight);
}
@Test
public void configure_setAspectRatioAndCrop_throwsIllegalStateException() {
Presentation.Builder presentationBuilder =
new Presentation.Builder()
.setAspectRatio(/* aspectRatio= */ 2f, Presentation.LAYOUT_SCALE_TO_FIT);
assertThrows(
IllegalStateException.class,
() ->
presentationBuilder.setCrop(
/* left= */ -.5f, /* right= */ .5f, /* bottom= */ .5f, /* top= */ 1f));
}
@Test
public void configure_setCropAndAspectRatio_throwsIllegalStateException() {
Presentation.Builder presentationBuilder =
new Presentation.Builder()
.setCrop(/* left= */ -.5f, /* right= */ .5f, /* bottom= */ .5f, /* top= */ 1f);
assertThrows(
IllegalStateException.class,
() ->
presentationBuilder.setAspectRatio(
/* aspectRatio= */ 2f, Presentation.LAYOUT_SCALE_TO_FIT));
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB