diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/CropPixelTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/CropPixelTest.java new file mode 100644 index 0000000000..a7c7db9b0a --- /dev/null +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/CropPixelTest.java @@ -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}. + * + *

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); + } +} diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameProcessorChainPixelTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameProcessorChainPixelTest.java index d6b95a0cb0..e55ab256c6 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameProcessorChainPixelTest.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/FrameProcessorChainPixelTest.java @@ -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 full10StepRotationAndCenterCrop = new ImmutableList.Builder<>(); for (int i = 0; i < 10; i++) { full10StepRotationAndCenterCrop.add(new Rotation(/* degrees= */ 36)); diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/PresentationPixelTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/PresentationPixelTest.java index 9bbe76934e..e04820ed9c 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/PresentationPixelTest.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/PresentationPixelTest.java @@ -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 { diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Crop.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Crop.java new file mode 100644 index 0000000000..f843f83a45 --- /dev/null +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Crop.java @@ -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. + * + *

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. + * + *

{@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"); + } +} diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Presentation.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Presentation.java index 67f5a565a1..c9d6c4efbd 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Presentation.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Presentation.java @@ -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). * - *

Cropping or aspect ratio is applied before setting resolution. + *

Aspect ratio is applied before setting resolution. * *

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. - * - *

{@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. - * - *

Width and height values set may be rescaled by {@link #setResolution(int)}, which is - * applied after cropping changes. - * - *

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 { *

Width and height values set may be rescaled by {@link #setResolution(int)}, which is * applied after aspect ratio changes. * - *

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; diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/CropTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/CropTest.java new file mode 100644 index 0000000000..172d9e81d6 --- /dev/null +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/CropTest.java @@ -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}. + * + *

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); + } +} diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/PresentationTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/PresentationTest.java index 5638ce3aa8..aa9ef2f303 100644 --- a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/PresentationTest.java +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/PresentationTest.java @@ -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)); - } } diff --git a/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/crop_then_aspect_ratio.png b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/crop_then_aspect_ratio.png new file mode 100644 index 0000000000..6813342d46 Binary files /dev/null and b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/crop_then_aspect_ratio.png differ