Add API to set resolution that respects orientation

This method allows a single Presentation Effect to resize portrait and landscape videos to a fixed resolution, while maintaining their orientations.

PiperOrigin-RevId: 737652909
This commit is contained in:
Googler 2025-03-17 09:57:18 -07:00 committed by Copybara-Service
parent 40ab0d40a1
commit 0c78bae111
8 changed files with 197 additions and 8 deletions

View File

@ -29,6 +29,9 @@
* DataSource:
* DRM:
* Effect:
* Add `Presentation.createForShortSide(int)` that creates a `Presentation`
that ensures the shortest side always matches the given value,
regardless of input orientation.
* Muxers:
* `writeSampleData()` API now uses muxer specific `BufferInfo` class
instead of `MediaCodec.BufferInfo`.

View File

@ -218,7 +218,7 @@ public final class CompositionPreviewActivity extends AppCompatActivity {
if (!SAME_AS_INPUT_OPTION.equals(selectedResolutionHeight)) {
int resolutionHeight = Integer.parseInt(selectedResolutionHeight);
videoEffectsBuilder.add(LanczosResample.scaleToFit(10000, resolutionHeight));
videoEffectsBuilder.add(Presentation.createForHeight(resolutionHeight));
videoEffectsBuilder.add(Presentation.createForShortSide(resolutionHeight));
}
ImmutableList<Effect> videoEffects = videoEffectsBuilder.build();
for (int i = 0; i < selectedMediaItems.length; i++) {

View File

@ -678,7 +678,7 @@ public final class TransformerActivity extends AppCompatActivity {
bundle.getInt(ConfigurationActivity.RESOLUTION_HEIGHT, /* defaultValue= */ C.LENGTH_UNSET);
if (resolutionHeight != C.LENGTH_UNSET) {
effects.add(LanczosResample.scaleToFit(10000, resolutionHeight));
effects.add(Presentation.createForHeight(resolutionHeight));
effects.add(Presentation.createForShortSide(resolutionHeight));
}
return effects.build();

View File

@ -61,6 +61,8 @@ public final class PresentationPixelTest {
private static final String ORIGINAL_PNG_ASSET_PATH =
"test-generated-goldens/sample_mp4_first_frame/electrical_colors/original.png";
private static final String ORIGINAL_PORTRAIT_PNG_ASSET_PATH =
"test-generated-goldens/sample_mp4_first_frame/electrical_colors/original_portrait.png";
private static final String ASPECT_RATIO_SCALE_TO_FIT_NARROW_PNG_ASSET_PATH =
"test-generated-goldens/sample_mp4_first_frame/electrical_colors/aspect_ratio_scale_to_fit_narrow.png";
private static final String ASPECT_RATIO_SCALE_TO_FIT_WIDE_PNG_ASSET_PATH =
@ -76,6 +78,8 @@ public final class PresentationPixelTest {
private static final String HIGH_RESOLUTION_JPG_ASSET_PATH = "media/jpeg/ultraHDR.jpg";
private static final String DOWNSCALED_6X_PNG_ASSET_PATH =
"test-generated-goldens/PresentationPixelTest/ultraHDR_mipmap_512x680.png";
private static final String UPSCALED_2X_PORTRAIT_PNG_ASSET_PATH =
"test-generated-goldens/sample_mp4_first_frame/electrical_colors/upscale_2x_portrait.png";
private final Context context = getApplicationContext();
@ -282,6 +286,95 @@ public final class PresentationPixelTest {
assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD);
}
@Test
public void drawFrame_createForShortSide_landscape_noEdits_matchesGoldenFile() throws Exception {
presentationShaderProgram =
Presentation.createForShortSide(inputHeight)
.toGlShaderProgram(context, /* useHdr= */ false);
Size outputSize = presentationShaderProgram.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
Bitmap expectedBitmap = readBitmap(ORIGINAL_PNG_ASSET_PATH);
presentationShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
createArgb8888BitmapFromFocusedGlFramebuffer(outputSize.getWidth(), outputSize.getHeight());
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null);
// TODO(b/207848601): Switch to using proper tooling for testing against golden data.
float averagePixelAbsoluteDifference =
getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId);
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void drawFrame_createForShortSide_portrait_noEdits_matchesGoldenFile() throws Exception {
Bitmap inputBitmap = readBitmap(ORIGINAL_PORTRAIT_PNG_ASSET_PATH);
inputWidth = inputBitmap.getWidth();
inputHeight = inputBitmap.getHeight();
inputTexId = createGlTextureFromBitmap(inputBitmap);
presentationShaderProgram =
Presentation.createForShortSide(inputWidth).toGlShaderProgram(context, /* useHdr= */ false);
Size outputSize = presentationShaderProgram.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
presentationShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
createArgb8888BitmapFromFocusedGlFramebuffer(outputSize.getWidth(), outputSize.getHeight());
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null);
// TODO(b/207848601): Switch to using proper tooling for testing against golden data.
float averagePixelAbsoluteDifference =
getBitmapAveragePixelAbsoluteDifferenceArgb8888(inputBitmap, actualBitmap, testId);
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void drawFrame_createForShortSide_portrait_upscale_matchesGoldenFile() throws Exception {
Bitmap inputBitmap = readBitmap(ORIGINAL_PORTRAIT_PNG_ASSET_PATH);
inputWidth = inputBitmap.getWidth();
inputHeight = inputBitmap.getHeight();
inputTexId = createGlTextureFromBitmap(inputBitmap);
presentationShaderProgram =
Presentation.createForShortSide(inputWidth * 2)
.toGlShaderProgram(context, /* useHdr= */ false);
Size outputSize = presentationShaderProgram.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
Bitmap expectedBitmap = readBitmap(UPSCALED_2X_PORTRAIT_PNG_ASSET_PATH);
presentationShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
createArgb8888BitmapFromFocusedGlFramebuffer(outputSize.getWidth(), outputSize.getHeight());
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null);
// TODO(b/207848601): Switch to using proper tooling for testing against golden data.
float averagePixelAbsoluteDifference =
getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId);
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void drawFrame_createForShortSide_portrait_downscaleWithLinearMipmap_matchesGoldenFile()
throws Exception {
Bitmap inputBitmap = readBitmap(HIGH_RESOLUTION_JPG_ASSET_PATH);
inputWidth = inputBitmap.getWidth();
inputHeight = inputBitmap.getHeight();
inputTexId = createGlTextureFromBitmap(inputBitmap);
presentationShaderProgram =
Presentation.createForShortSide(inputWidth / 6)
.copyWithTextureMinFilter(C.TEXTURE_MIN_FILTER_LINEAR_MIPMAP_LINEAR)
.toGlShaderProgram(context, /* useHdr= */ false);
Size outputSize = presentationShaderProgram.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
Bitmap expectedBitmap = readBitmap(DOWNSCALED_6X_PNG_ASSET_PATH);
presentationShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
createArgb8888BitmapFromFocusedGlFramebuffer(outputSize.getWidth(), outputSize.getHeight());
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null);
assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD);
}
private void setupOutputTexture(int outputWidth, int outputHeight) throws GlUtil.GlException {
int outputTexId =
GlUtil.createTexture(

View File

@ -131,7 +131,8 @@ public final class Presentation implements MatrixTransformation {
/* height= */ C.LENGTH_UNSET,
aspectRatio,
layout,
TEXTURE_MIN_FILTER_LINEAR);
TEXTURE_MIN_FILTER_LINEAR,
/* preservePortraitWhenApplicable= */ false);
}
/**
@ -148,7 +149,8 @@ public final class Presentation implements MatrixTransformation {
height,
ASPECT_RATIO_UNSET,
LAYOUT_SCALE_TO_FIT,
TEXTURE_MIN_FILTER_LINEAR);
TEXTURE_MIN_FILTER_LINEAR,
/* preservePortraitWhenApplicable= */ false);
}
/**
@ -166,7 +168,33 @@ public final class Presentation implements MatrixTransformation {
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, TEXTURE_MIN_FILTER_LINEAR);
return new Presentation(
width,
height,
ASPECT_RATIO_UNSET,
layout,
TEXTURE_MIN_FILTER_LINEAR,
/* preservePortraitWhenApplicable= */ false);
}
/**
* Creates a new {@link Presentation} instance.
*
* <p>The output frame will have a short side matching the given value. The longest side will
* scale to preserve the input aspect * ratio. For example, passing a shortSide of 480 will scale
* a 1440x1920 video to 480x640 or a 1920x1440 video to 640x480.
*
* @param shortSide The length of the short side of the output frame, in pixels.
*/
public static Presentation createForShortSide(int shortSide) {
checkArgument(shortSide > 0, "shortSide " + shortSide + " must be positive");
return new Presentation(
/* width= */ C.LENGTH_UNSET,
/* height= */ shortSide,
ASPECT_RATIO_UNSET,
LAYOUT_SCALE_TO_FIT,
TEXTURE_MIN_FILTER_LINEAR,
/* preservePortraitWhenApplicable= */ true);
}
private final int requestedWidthPixels;
@ -174,6 +202,7 @@ public final class Presentation implements MatrixTransformation {
private float requestedAspectRatio;
private final @Layout int layout;
private final @C.TextureMinFilter int textureMinFilter;
private final boolean preservePortraitWhenApplicable;
private float outputWidth;
private float outputHeight;
@ -184,7 +213,8 @@ public final class Presentation implements MatrixTransformation {
int height,
float aspectRatio,
@Layout int layout,
@C.TextureMinFilter int textureMinFilter) {
@C.TextureMinFilter int textureMinFilter,
boolean preservePortraitWhenApplicable) {
checkArgument(
(aspectRatio == ASPECT_RATIO_UNSET) || (width == C.LENGTH_UNSET),
"width and aspect ratio should not both be set");
@ -194,6 +224,7 @@ public final class Presentation implements MatrixTransformation {
this.requestedAspectRatio = aspectRatio;
this.layout = layout;
this.textureMinFilter = textureMinFilter;
this.preservePortraitWhenApplicable = preservePortraitWhenApplicable;
outputWidth = C.LENGTH_UNSET;
outputHeight = C.LENGTH_UNSET;
@ -214,7 +245,8 @@ public final class Presentation implements MatrixTransformation {
requestedHeightPixels,
requestedAspectRatio,
layout,
textureMinFilter);
textureMinFilter,
preservePortraitWhenApplicable);
}
@Override
@ -243,10 +275,15 @@ public final class Presentation implements MatrixTransformation {
if (requestedHeightPixels != C.LENGTH_UNSET) {
if (requestedWidthPixels != C.LENGTH_UNSET) {
outputWidth = requestedWidthPixels;
outputHeight = requestedHeightPixels;
} else if (preservePortraitWhenApplicable && inputHeight > inputWidth) {
// Swap width and height if the input orientation should be respected.
outputHeight = requestedHeightPixels * outputHeight / outputWidth;
outputWidth = requestedHeightPixels;
} else {
outputWidth = requestedHeightPixels * outputWidth / outputHeight;
outputHeight = requestedHeightPixels;
}
outputHeight = requestedHeightPixels;
}
return new Size(Math.round(outputWidth), Math.round(outputHeight));
}

View File

@ -92,4 +92,60 @@ public final class PresentationTest {
assertThat(outputSize.getWidth()).isEqualTo(requestedWidth);
assertThat(outputSize.getHeight()).isEqualTo(requestedHeight);
}
@Test
public void configure_createForShortSideWithPortraitInput_changesDimension() {
int inputWidth = 720;
int inputHeight = 1280;
Presentation presentation = Presentation.createForShortSide(1080);
Size outputSize = presentation.configure(inputWidth, inputHeight);
boolean isNoOp = presentation.isNoOp(inputWidth, inputHeight);
assertThat(isNoOp).isFalse();
assertThat(outputSize.getWidth()).isEqualTo(1080);
assertThat(outputSize.getHeight()).isEqualTo(1920);
}
@Test
public void configure_createForShortSideWithPortraitInputNoEdit_leavesFramesUnchanged() {
int inputWidth = 720;
int inputHeight = 1280;
Presentation presentation = Presentation.createForShortSide(inputWidth);
Size outputSize = presentation.configure(inputWidth, inputHeight);
boolean isNoOp = presentation.isNoOp(inputWidth, inputHeight);
assertThat(isNoOp).isTrue();
assertThat(outputSize.getWidth()).isEqualTo(inputWidth);
assertThat(outputSize.getHeight()).isEqualTo(inputHeight);
}
@Test
public void configure_createForShortSideWithLandscapeInput_changesDimension() {
int inputWidth = 1280;
int inputHeight = 720;
Presentation presentation = Presentation.createForShortSide(1080);
Size outputSize = presentation.configure(inputWidth, inputHeight);
boolean isNoOp = presentation.isNoOp(inputWidth, inputHeight);
assertThat(isNoOp).isFalse();
assertThat(outputSize.getWidth()).isEqualTo(1920);
assertThat(outputSize.getHeight()).isEqualTo(1080);
}
@Test
public void configure_createForShortSideWithLandscapeInputNoEdit_leavesFramesUnchanged() {
int inputWidth = 1280;
int inputHeight = 720;
Presentation presentation = Presentation.createForShortSide(720);
Size outputSize = presentation.configure(inputWidth, inputHeight);
boolean isNoOp = presentation.isNoOp(inputWidth, inputHeight);
assertThat(isNoOp).isTrue();
assertThat(outputSize.getWidth()).isEqualTo(1280);
assertThat(outputSize.getHeight()).isEqualTo(720);
}
}