diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainPixelTest.java index 0820049cb9..263714eca3 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainPixelTest.java @@ -204,10 +204,10 @@ public final class FrameProcessorChainPixelTest { } @Test - public void processData_withPresentation_setResolution_producesExpectedOutput() throws Exception { - String testId = "processData_withPresentation_setResolution"; - setUpAndPrepareFirstFrame( - DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO, new Presentation.Builder().setResolution(480).build()); + public void processData_withPresentation_createForHeight_producesExpectedOutput() + throws Exception { + String testId = "processData_withPresentation_createForHeight"; + setUpAndPrepareFirstFrame(DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO, Presentation.createForHeight(480)); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(REQUEST_OUTPUT_HEIGHT_PNG_ASSET_PATH); Bitmap actualBitmap = processFirstFrameAndEnd(); @@ -222,14 +222,13 @@ public final class FrameProcessorChainPixelTest { } @Test - public void processData_withCropAndPresentation_producesExpectedOutput() throws Exception { - String testId = "processData_withCropAndPresentation"; + public void processData_withCropThenPresentation_producesExpectedOutput() throws Exception { + String testId = "processData_withCropThenPresentation"; 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()); + Presentation.createForAspectRatio( + /* aspectRatio= */ .5f, Presentation.LAYOUT_SCALE_TO_FIT)); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(CROP_THEN_ASPECT_RATIO_PNG_ASSET_PATH); Bitmap actualBitmap = processFirstFrameAndEnd(); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/PresentationPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/PresentationPixelTest.java index 867aed28ff..53bbbea9ee 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/PresentationPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/PresentationPixelTest.java @@ -26,6 +26,7 @@ import android.opengl.EGLContext; import android.opengl.EGLDisplay; import android.opengl.EGLSurface; import android.util.Size; +import androidx.media3.common.C; import androidx.media3.common.util.GlUtil; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.io.IOException; @@ -95,7 +96,8 @@ public final class PresentationPixelTest { @Test public void drawFrame_noEdits_producesExpectedOutput() throws Exception { String testId = "drawFrame_noEdits"; - presentationTextureProcessor = new Presentation.Builder().build().toGlTextureProcessor(context); + presentationTextureProcessor = + Presentation.createForHeight(C.LENGTH_UNSET).toGlTextureProcessor(context); Size outputSize = presentationTextureProcessor.configure(inputWidth, inputHeight); setupOutputTexture(outputSize.getWidth(), outputSize.getHeight()); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH); @@ -119,9 +121,7 @@ public final class PresentationPixelTest { throws Exception { String testId = "drawFrame_changeAspectRatio_scaleToFit_narrow"; presentationTextureProcessor = - new Presentation.Builder() - .setAspectRatio(1f, Presentation.LAYOUT_SCALE_TO_FIT) - .build() + Presentation.createForAspectRatio(/* aspectRatio= */ 1f, Presentation.LAYOUT_SCALE_TO_FIT) .toGlTextureProcessor(context); Size outputSize = presentationTextureProcessor.configure(inputWidth, inputHeight); setupOutputTexture(outputSize.getWidth(), outputSize.getHeight()); @@ -147,9 +147,7 @@ public final class PresentationPixelTest { throws Exception { String testId = "drawFrame_changeAspectRatio_scaleToFit_wide"; presentationTextureProcessor = - new Presentation.Builder() - .setAspectRatio(2f, Presentation.LAYOUT_SCALE_TO_FIT) - .build() + Presentation.createForAspectRatio(/* aspectRatio= */ 2f, Presentation.LAYOUT_SCALE_TO_FIT) .toGlTextureProcessor(context); Size outputSize = presentationTextureProcessor.configure(inputWidth, inputHeight); setupOutputTexture(outputSize.getWidth(), outputSize.getHeight()); @@ -175,9 +173,8 @@ public final class PresentationPixelTest { throws Exception { String testId = "drawFrame_changeAspectRatio_scaleToFitWithCrop_narrow"; presentationTextureProcessor = - new Presentation.Builder() - .setAspectRatio(1f, Presentation.LAYOUT_SCALE_TO_FIT_WITH_CROP) - .build() + Presentation.createForAspectRatio( + /* aspectRatio= */ 1f, Presentation.LAYOUT_SCALE_TO_FIT_WITH_CROP) .toGlTextureProcessor(context); Size outputSize = presentationTextureProcessor.configure(inputWidth, inputHeight); setupOutputTexture(outputSize.getWidth(), outputSize.getHeight()); @@ -203,9 +200,8 @@ public final class PresentationPixelTest { throws Exception { String testId = "drawFrame_changeAspectRatio_scaleToFitWithCrop_wide"; presentationTextureProcessor = - new Presentation.Builder() - .setAspectRatio(2f, Presentation.LAYOUT_SCALE_TO_FIT_WITH_CROP) - .build() + Presentation.createForAspectRatio( + /* aspectRatio= */ 2f, Presentation.LAYOUT_SCALE_TO_FIT_WITH_CROP) .toGlTextureProcessor(context); Size outputSize = presentationTextureProcessor.configure(inputWidth, inputHeight); setupOutputTexture(outputSize.getWidth(), outputSize.getHeight()); @@ -231,9 +227,7 @@ public final class PresentationPixelTest { throws Exception { String testId = "drawFrame_changeAspectRatio_stretchToFit_narrow"; presentationTextureProcessor = - new Presentation.Builder() - .setAspectRatio(1f, Presentation.LAYOUT_STRETCH_TO_FIT) - .build() + Presentation.createForAspectRatio(/* aspectRatio= */ 1f, Presentation.LAYOUT_STRETCH_TO_FIT) .toGlTextureProcessor(context); Size outputSize = presentationTextureProcessor.configure(inputWidth, inputHeight); setupOutputTexture(outputSize.getWidth(), outputSize.getHeight()); @@ -259,9 +253,7 @@ public final class PresentationPixelTest { throws Exception { String testId = "drawFrame_changeAspectRatio_stretchToFit_wide"; presentationTextureProcessor = - new Presentation.Builder() - .setAspectRatio(2f, Presentation.LAYOUT_STRETCH_TO_FIT) - .build() + Presentation.createForAspectRatio(/* aspectRatio= */ 2f, Presentation.LAYOUT_STRETCH_TO_FIT) .toGlTextureProcessor(context); Size outputSize = presentationTextureProcessor.configure(inputWidth, inputHeight); setupOutputTexture(outputSize.getWidth(), outputSize.getHeight()); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java index f86eda288c..8476dadabb 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java @@ -259,12 +259,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (outputSurfaceInfo.width != outputSize.getWidth() || outputSurfaceInfo.height != outputSize.getHeight()) { matrixTransformationListBuilder.add( - new Presentation.Builder() - .setAspectRatio( - outputSurfaceInfo.width / (float) outputSurfaceInfo.height, - Presentation.LAYOUT_SCALE_TO_FIT) - .setResolution(outputSurfaceInfo.height) - .build()); + Presentation.createForWidthAndHeight( + outputSurfaceInfo.width, outputSurfaceInfo.height, Presentation.LAYOUT_SCALE_TO_FIT)); } // Convert final list of matrix transformations (including additional transformations for the diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Presentation.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Presentation.java index 167e83bf0f..077740aa81 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Presentation.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Presentation.java @@ -36,8 +36,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * 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). * - *

Aspect ratio is applied before setting resolution. - * *

The background color of the output frame will be black, with alpha = 0 if applicable. */ @UnstableApi @@ -49,8 +47,7 @@ public final class Presentation implements MatrixTransformation { *

One of {@link #LAYOUT_SCALE_TO_FIT}, {@link #LAYOUT_SCALE_TO_FIT_WITH_CROP}, or {@link * #LAYOUT_STRETCH_TO_FIT}. * - *

May scale either width or height, leaving the other output dimension equal to its input, - * unless {@link Builder#setResolution(int)} rescales width and height. + *

May scale either width or height, leaving the other output dimension equal to its input. */ @Documented @Retention(SOURCE) @@ -100,81 +97,82 @@ public final class Presentation implements MatrixTransformation { */ public static final int LAYOUT_STRETCH_TO_FIT = 2; - /** A builder for {@link Presentation} instances. */ - public static final class Builder { + private static final float ASPECT_RATIO_UNSET = -1f; - // Optional fields. - private int outputHeight; - private float aspectRatio; - private @Layout int layout; - - /** Creates a builder with default values. */ - public Builder() { - outputHeight = C.LENGTH_UNSET; - aspectRatio = C.LENGTH_UNSET; - } - - /** - * Sets the output resolution using the output height. - * - *

The default value, {@link C#LENGTH_UNSET}, corresponds to using the same height as the - * input. Output width of the displayed frame will scale to preserve the frame's aspect ratio - * after other transformations. - * - *

For example, a 1920x1440 frame can be scaled to 640x480 by calling {@code - * setResolution(480)}. - * - * @param height The output height of the displayed frame, in pixels. - * @return This builder. - */ - public Builder setResolution(int height) { - this.outputHeight = height; - return this; - } - - /** - * Sets the aspect ratio (width/height ratio) for the output frame. - * - *

Resizes a frame's width or height to conform to an {@code aspectRatio}, given a {@link - * Layout}. {@code aspectRatio} defaults to {@link C#LENGTH_UNSET}, which corresponds to the - * same aspect ratio as the input frame. {@code layout} defaults to {@link #LAYOUT_SCALE_TO_FIT} - * - *

Width and height values set may be rescaled by {@link #setResolution(int)}, which is - * applied after aspect ratio changes. - * - * @param aspectRatio The aspect ratio (width/height ratio) of the output frame. Must be - * positive. - * @return This builder. - */ - public Builder setAspectRatio(float aspectRatio, @Layout int layout) { - checkArgument(aspectRatio > 0, "aspect ratio " + aspectRatio + " must be positive"); - checkArgument( - layout == LAYOUT_SCALE_TO_FIT - || layout == LAYOUT_SCALE_TO_FIT_WITH_CROP - || layout == LAYOUT_STRETCH_TO_FIT, - "invalid layout " + layout); - this.aspectRatio = aspectRatio; - this.layout = layout; - return this; - } - - public Presentation build() { - return new Presentation(outputHeight, aspectRatio, layout); - } + private static void checkLayout(@Layout int layout) { + checkArgument( + layout == LAYOUT_SCALE_TO_FIT + || layout == LAYOUT_SCALE_TO_FIT_WITH_CROP + || layout == LAYOUT_STRETCH_TO_FIT, + "invalid layout " + layout); } + /** + * Creates a new {@link Presentation} instance. + * + *

The output frame will have the given aspect ratio (width/height ratio). Width or height will + * be resized to conform to this {@code aspectRatio}, given a {@link Layout}. + * + * @param aspectRatio The aspect ratio (width/height ratio) of the output frame. Must be positive. + * @param layout The layout of the output frame. + */ + public static Presentation createForAspectRatio(float aspectRatio, @Layout int layout) { + checkArgument( + aspectRatio == C.LENGTH_UNSET || aspectRatio > 0, + "aspect ratio " + aspectRatio + " must be positive or unset"); + checkLayout(layout); + return new Presentation( + /* width= */ C.LENGTH_UNSET, /* height= */ C.LENGTH_UNSET, aspectRatio, layout); + } + + /** + * Creates a new {@link Presentation} instance. + * + *

The output frame will have the given height. Width will scale to preserve the input aspect + * ratio. + * + * @param height The height of the output frame, in pixels. + */ + public static Presentation createForHeight(int height) { + return new Presentation( + /* width= */ C.LENGTH_UNSET, height, ASPECT_RATIO_UNSET, LAYOUT_SCALE_TO_FIT); + } + + /** + * Creates a new {@link Presentation} instance. + * + *

The output frame will have the given width and height, given a {@link Layout}. + * + *

Width and height must be positive integers representing the output frame's width and height. + * + * @param width The width of the output frame, in pixels. + * @param height The height of the output frame, in pixels. + * @param layout The layout of the output frame. + */ + public static Presentation createForWidthAndHeight(int width, int height, @Layout int layout) { + checkArgument(width > 0, "width " + width + " must be positive"); + checkArgument(height > 0, "height " + height + " must be positive"); + checkLayout(layout); + return new Presentation(width, height, ASPECT_RATIO_UNSET, layout); + } + + private final int requestedWidthPixels; private final int requestedHeightPixels; - private final float requestedAspectRatio; + private float requestedAspectRatio; private final @Layout int layout; private float outputWidth; private float outputHeight; private @MonotonicNonNull Matrix transformationMatrix; - /** Creates a new instance. */ - private Presentation(int requestedHeightPixels, float requestedAspectRatio, @Layout int layout) { - this.requestedHeightPixels = requestedHeightPixels; - this.requestedAspectRatio = requestedAspectRatio; + private Presentation(int width, int height, float aspectRatio, @Layout int layout) { + checkArgument( + (aspectRatio == C.LENGTH_UNSET) || (width == C.LENGTH_UNSET), + "width and aspect ratio should not both be set"); + + this.requestedWidthPixels = width; + this.requestedHeightPixels = height; + this.requestedAspectRatio = aspectRatio; this.layout = layout; outputWidth = C.LENGTH_UNSET; @@ -191,13 +189,21 @@ public final class Presentation implements MatrixTransformation { outputWidth = inputWidth; outputHeight = inputHeight; + if ((requestedWidthPixels != C.LENGTH_UNSET) && (requestedHeightPixels != C.LENGTH_UNSET)) { + requestedAspectRatio = (float) requestedWidthPixels / requestedHeightPixels; + } + if (requestedAspectRatio != C.LENGTH_UNSET) { applyAspectRatio(); } - // Scale width and height to desired requestedHeightPixels, preserving aspect ratio. - if (requestedHeightPixels != C.LENGTH_UNSET && requestedHeightPixels != outputHeight) { - outputWidth = requestedHeightPixels * outputWidth / outputHeight; + // Scale output width and height to requested values. + if (requestedHeightPixels != C.LENGTH_UNSET) { + if (requestedWidthPixels != C.LENGTH_UNSET) { + outputWidth = requestedWidthPixels; + } else { + outputWidth = requestedHeightPixels * outputWidth / outputHeight; + } outputHeight = requestedHeightPixels; } return new Size(Math.round(outputWidth), Math.round(outputHeight)); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java index 6c05da6c51..2a7a9aa37c 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java @@ -88,8 +88,7 @@ import org.checkerframework.dataflow.qual.Pure; .build()); } if (transformationRequest.outputHeight != C.LENGTH_UNSET) { - effectsListBuilder.add( - new Presentation.Builder().setResolution(transformationRequest.outputHeight).build()); + effectsListBuilder.add(Presentation.createForHeight(transformationRequest.outputHeight)); } AtomicReference encoderInitializationException = diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/PresentationTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/PresentationTest.java index 60f857c059..6631ab3cef 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/PresentationTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/PresentationTest.java @@ -18,6 +18,7 @@ package androidx.media3.transformer; import static com.google.common.truth.Truth.assertThat; import android.util.Size; +import androidx.media3.common.C; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,7 +34,7 @@ public final class PresentationTest { public void configure_noEdits_leavesFramesUnchanged() { int inputWidth = 200; int inputHeight = 150; - Presentation presentation = new Presentation.Builder().build(); + Presentation presentation = Presentation.createForHeight(C.LENGTH_UNSET); Size outputSize = presentation.configure(inputWidth, inputHeight); @@ -42,11 +43,11 @@ public final class PresentationTest { } @Test - public void configure_setResolution_changesDimensions() { + public void configure_createForHeight_changesDimensions() { int inputWidth = 200; int inputHeight = 150; int requestedHeight = 300; - Presentation presentation = new Presentation.Builder().setResolution(requestedHeight).build(); + Presentation presentation = Presentation.createForHeight(requestedHeight); Size outputSize = presentation.configure(inputWidth, inputHeight); @@ -55,14 +56,12 @@ public final class PresentationTest { } @Test - public void configure_setAspectRatio_changesDimensions() { + public void configure_createForAspectRatio_changesDimensions() { int inputWidth = 300; int inputHeight = 200; float aspectRatio = 2f; Presentation presentation = - new Presentation.Builder() - .setAspectRatio(aspectRatio, Presentation.LAYOUT_SCALE_TO_FIT) - .build(); + Presentation.createForAspectRatio(aspectRatio, Presentation.LAYOUT_SCALE_TO_FIT); Size outputSize = presentation.configure(inputWidth, inputHeight); @@ -71,20 +70,18 @@ public final class PresentationTest { } @Test - public void configure_setAspectRatioAndResolution_changesDimensions() { + public void configure_createForWidthAndHeight_changesDimensions() { int inputWidth = 300; int inputHeight = 200; - float aspectRatio = 2f; - int requestedHeight = 100; + int requestedWidth = 100; + int requestedHeight = 300; Presentation presentation = - new Presentation.Builder() - .setAspectRatio(aspectRatio, Presentation.LAYOUT_SCALE_TO_FIT) - .setResolution(requestedHeight) - .build(); + Presentation.createForWidthAndHeight( + requestedWidth, requestedHeight, Presentation.LAYOUT_SCALE_TO_FIT); Size outputSize = presentation.configure(inputWidth, inputHeight); - assertThat(outputSize.getWidth()).isEqualTo(Math.round(aspectRatio * requestedHeight)); + assertThat(outputSize.getWidth()).isEqualTo(requestedWidth); assertThat(outputSize.getHeight()).isEqualTo(requestedHeight); } }