mirror of
https://github.com/androidx/media.git
synced 2025-05-08 16:10:38 +08:00
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:
parent
b3b57bc93d
commit
b33dc5e57b
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
@ -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 {
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
BIN
testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/crop_then_aspect_ratio.png
vendored
Normal file
BIN
testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/crop_then_aspect_ratio.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 183 KiB |
Loading…
x
Reference in New Issue
Block a user