mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
40ab0d40a1
commit
0c78bae111
@ -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`.
|
||||
|
@ -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++) {
|
||||
|
@ -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();
|
||||
|
@ -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(
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 670 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.7 MiB |
Loading…
x
Reference in New Issue
Block a user