Transformer GL: Implement auto-scaling to preserve input frame.
PiperOrigin-RevId: 427982223
This commit is contained in:
parent
8b180eb040
commit
b5ed01d479
@ -152,7 +152,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
|||||||
rotateAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
rotateAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||||
rotateSpinner = findViewById(R.id.rotate_spinner);
|
rotateSpinner = findViewById(R.id.rotate_spinner);
|
||||||
rotateSpinner.setAdapter(rotateAdapter);
|
rotateSpinner.setAdapter(rotateAdapter);
|
||||||
rotateAdapter.addAll(SAME_AS_INPUT_OPTION, "0", "10", "45", "90", "180");
|
rotateAdapter.addAll(SAME_AS_INPUT_OPTION, "0", "10", "45", "60", "90", "180");
|
||||||
|
|
||||||
enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox);
|
enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox);
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory {
|
|||||||
checkArgument(format.width != Format.NO_VALUE);
|
checkArgument(format.width != Format.NO_VALUE);
|
||||||
checkArgument(format.height != Format.NO_VALUE);
|
checkArgument(format.height != Format.NO_VALUE);
|
||||||
// According to interface Javadoc, format.rotationDegrees should be 0. The video should always
|
// According to interface Javadoc, format.rotationDegrees should be 0. The video should always
|
||||||
// be in landscape orientation.
|
// be encoded in landscape orientation.
|
||||||
checkArgument(format.height <= format.width);
|
checkArgument(format.height <= format.width);
|
||||||
checkArgument(format.rotationDegrees == 0);
|
checkArgument(format.rotationDegrees == 0);
|
||||||
checkNotNull(format.sampleMimeType);
|
checkNotNull(format.sampleMimeType);
|
||||||
|
@ -26,7 +26,6 @@ import androidx.media3.common.util.UnstableApi;
|
|||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.exoplayer.source.MediaSource;
|
import androidx.media3.exoplayer.source.MediaSource;
|
||||||
import androidx.media3.extractor.mp4.Mp4Extractor;
|
import androidx.media3.extractor.mp4.Mp4Extractor;
|
||||||
import com.google.common.collect.ImmutableSet;
|
|
||||||
|
|
||||||
/** A media transformation request. */
|
/** A media transformation request. */
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@ -35,9 +34,6 @@ public final class TransformationRequest {
|
|||||||
/** A builder for {@link TransformationRequest} instances. */
|
/** A builder for {@link TransformationRequest} instances. */
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
|
|
||||||
private static final ImmutableSet<Integer> SUPPORTED_OUTPUT_HEIGHTS =
|
|
||||||
ImmutableSet.of(144, 240, 360, 480, 720, 1080, 1440, 2160);
|
|
||||||
|
|
||||||
private Matrix transformationMatrix;
|
private Matrix transformationMatrix;
|
||||||
private boolean flattenForSlowMotion;
|
private boolean flattenForSlowMotion;
|
||||||
private int outputHeight;
|
private int outputHeight;
|
||||||
@ -124,27 +120,17 @@ public final class TransformationRequest {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the output resolution using the output height. The default value {@link 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 will scale to preserve the
|
* corresponds to using the same height as the input. Output width of the displayed video will
|
||||||
* input video's aspect ratio.
|
* scale to preserve the video's aspect ratio after other transformations.
|
||||||
*
|
|
||||||
* <p>For now, only "popular" heights like 144, 240, 360, 480, 720, 1080, 1440, or 2160 are
|
|
||||||
* supported, to ensure compatibility on different devices.
|
|
||||||
*
|
*
|
||||||
* <p>For example, a 1920x1440 video can be scaled to 640x480 by calling setResolution(480).
|
* <p>For example, a 1920x1440 video can be scaled to 640x480 by calling setResolution(480).
|
||||||
*
|
*
|
||||||
* @param outputHeight The output height in pixels.
|
* @param outputHeight The output height of the displayed video, in pixels.
|
||||||
* @return This builder.
|
* @return This builder.
|
||||||
* @throws IllegalArgumentException If the {@code outputHeight} is not supported.
|
* @throws IllegalArgumentException If the {@code outputHeight} is not supported.
|
||||||
*/
|
*/
|
||||||
public Builder setResolution(int outputHeight) {
|
public Builder setResolution(int outputHeight) {
|
||||||
// TODO(b/209781577): Define outputHeight in the javadoc as height can be ambiguous for videos
|
|
||||||
// where rotationDegrees is set in the Format.
|
|
||||||
// TODO(b/201293185): Restructure to input a Presentation class.
|
// TODO(b/201293185): Restructure to input a Presentation class.
|
||||||
// TODO(b/201293185): Check encoder codec capabilities in order to allow arbitrary
|
|
||||||
// resolutions and reasonable fallbacks.
|
|
||||||
checkArgument(
|
|
||||||
outputHeight == C.LENGTH_UNSET || SUPPORTED_OUTPUT_HEIGHTS.contains(outputHeight),
|
|
||||||
"Unsupported outputHeight: " + outputHeight);
|
|
||||||
this.outputHeight = outputHeight;
|
this.outputHeight = outputHeight;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@ package androidx.media3.transformer;
|
|||||||
|
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
import static androidx.media3.common.util.Util.SDK_INT;
|
import static androidx.media3.common.util.Util.SDK_INT;
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Matrix;
|
import android.graphics.Matrix;
|
||||||
@ -63,76 +65,106 @@ import org.checkerframework.dataflow.qual.Pure;
|
|||||||
encoderOutputBuffer =
|
encoderOutputBuffer =
|
||||||
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
|
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
|
||||||
|
|
||||||
// Scale width and height to desired transformationRequest.outputHeight, preserving aspect
|
// The decoder rotates encoded frames for display by inputFormat.rotationDegrees.
|
||||||
// ratio.
|
int decodedWidth =
|
||||||
// TODO(b/209781577): Think about which edge length should be set for portrait videos.
|
(inputFormat.rotationDegrees % 180 == 0) ? inputFormat.width : inputFormat.height;
|
||||||
float inputFormatAspectRatio = (float) inputFormat.width / inputFormat.height;
|
int decodedHeight =
|
||||||
int outputWidth = inputFormat.width;
|
(inputFormat.rotationDegrees % 180 == 0) ? inputFormat.height : inputFormat.width;
|
||||||
int outputHeight = inputFormat.height;
|
float decodedAspectRatio = (float) decodedWidth / decodedHeight;
|
||||||
|
|
||||||
|
Matrix transformationMatrix = new Matrix(transformationRequest.transformationMatrix);
|
||||||
|
|
||||||
|
int outputWidth = decodedWidth;
|
||||||
|
int outputHeight = decodedHeight;
|
||||||
|
if (!transformationMatrix.isIdentity()) {
|
||||||
|
// Scale frames by decodedAspectRatio, to account for FrameEditor's normalized device
|
||||||
|
// coordinates (NDC) (a square from -1 to 1 for both x and y) and preserve rectangular display
|
||||||
|
// of input pixels during transformations (ex. rotations). With scaling, transformationMatrix
|
||||||
|
// operations operate on a rectangle for x from -decodedAspectRatio to decodedAspectRatio, and
|
||||||
|
// y from -1 to 1.
|
||||||
|
transformationMatrix.preScale(/* sx= */ decodedAspectRatio, /* sy= */ 1f);
|
||||||
|
transformationMatrix.postScale(/* sx= */ 1f / decodedAspectRatio, /* sy= */ 1f);
|
||||||
|
|
||||||
|
float[][] transformOnNdcPoints = {{-1, -1, 0, 1}, {-1, 1, 0, 1}, {1, -1, 0, 1}, {1, 1, 0, 1}};
|
||||||
|
float xMin = Float.MAX_VALUE;
|
||||||
|
float xMax = Float.MIN_VALUE;
|
||||||
|
float yMin = Float.MAX_VALUE;
|
||||||
|
float yMax = Float.MIN_VALUE;
|
||||||
|
for (float[] transformOnNdcPoint : transformOnNdcPoints) {
|
||||||
|
transformationMatrix.mapPoints(transformOnNdcPoint);
|
||||||
|
xMin = min(xMin, transformOnNdcPoint[0]);
|
||||||
|
xMax = max(xMax, transformOnNdcPoint[0]);
|
||||||
|
yMin = min(yMin, transformOnNdcPoint[1]);
|
||||||
|
yMax = max(yMax, transformOnNdcPoint[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
float xCenter = (xMax + xMin) / 2f;
|
||||||
|
float yCenter = (yMax + yMin) / 2f;
|
||||||
|
transformationMatrix.postTranslate(-xCenter, -yCenter);
|
||||||
|
|
||||||
|
float ndcWidthAndHeight = 2f; // Length from -1 to 1.
|
||||||
|
float xScale = (xMax - xMin) / ndcWidthAndHeight;
|
||||||
|
float yScale = (yMax - yMin) / ndcWidthAndHeight;
|
||||||
|
transformationMatrix.postScale(1f / xScale, 1f / yScale);
|
||||||
|
outputWidth = Math.round(decodedWidth * xScale);
|
||||||
|
outputHeight = Math.round(decodedHeight * yScale);
|
||||||
|
}
|
||||||
|
// Scale width and height to desired transformationRequest.outputHeight, preserving
|
||||||
|
// aspect ratio.
|
||||||
if (transformationRequest.outputHeight != C.LENGTH_UNSET
|
if (transformationRequest.outputHeight != C.LENGTH_UNSET
|
||||||
&& transformationRequest.outputHeight != inputFormat.height) {
|
&& transformationRequest.outputHeight != outputHeight) {
|
||||||
outputWidth = Math.round(inputFormatAspectRatio * transformationRequest.outputHeight);
|
outputWidth =
|
||||||
|
Math.round((float) transformationRequest.outputHeight * outputWidth / outputHeight);
|
||||||
outputHeight = transformationRequest.outputHeight;
|
outputHeight = transformationRequest.outputHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The encoder may not support encoding in portrait orientation, so the decoded video is
|
// Encoders commonly support higher maximum widths than maximum heights. Rotate the decoded
|
||||||
// rotated to landscape orientation and a rotation is added back later to the output format.
|
// video before encoding, so the encoded video's width >= height, and set outputRotationDegrees
|
||||||
boolean swapEncodingDimensions = inputFormat.height > inputFormat.width;
|
// to ensure the video is displayed in the correct orientation.
|
||||||
|
int requestedEncoderWidth;
|
||||||
|
int requestedEncoderHeight;
|
||||||
|
boolean swapEncodingDimensions = outputHeight > outputWidth;
|
||||||
if (swapEncodingDimensions) {
|
if (swapEncodingDimensions) {
|
||||||
outputRotationDegrees = (inputFormat.rotationDegrees + 90) % 360;
|
outputRotationDegrees = 90;
|
||||||
int temp = outputWidth;
|
requestedEncoderWidth = outputHeight;
|
||||||
outputWidth = outputHeight;
|
requestedEncoderHeight = outputWidth;
|
||||||
outputHeight = temp;
|
// TODO(b/201293185): After fragment shader transformations are implemented, put
|
||||||
|
// postRotate in a later vertex shader.
|
||||||
|
transformationMatrix.postRotate(outputRotationDegrees);
|
||||||
} else {
|
} else {
|
||||||
outputRotationDegrees = inputFormat.rotationDegrees;
|
outputRotationDegrees = 0;
|
||||||
|
requestedEncoderWidth = outputWidth;
|
||||||
|
requestedEncoderHeight = outputHeight;
|
||||||
}
|
}
|
||||||
float displayAspectRatio =
|
|
||||||
(inputFormat.rotationDegrees % 180) == 0
|
|
||||||
? inputFormatAspectRatio
|
|
||||||
: 1.0f / inputFormatAspectRatio;
|
|
||||||
|
|
||||||
Matrix transformationMatrix = new Matrix(transformationRequest.transformationMatrix);
|
Format requestedEncoderFormat =
|
||||||
// Scale frames by input aspect ratio, to account for FrameEditor's square normalized device
|
|
||||||
// coordinates (-1 to 1) and preserve frame relative dimensions during transformations
|
|
||||||
// (ex. rotations). After this scaling, transformationMatrix operations operate on a rectangle
|
|
||||||
// for x from -displayAspectRatio to displayAspectRatio, and y from -1 to 1
|
|
||||||
transformationMatrix.preScale(displayAspectRatio, 1);
|
|
||||||
transformationMatrix.postScale(1.0f / displayAspectRatio, 1);
|
|
||||||
|
|
||||||
// The decoder rotates videos to their intended display orientation. The frameEditor rotates
|
|
||||||
// them back for improved encoder compatibility.
|
|
||||||
// TODO(b/201293185): After fragment shader transformations are implemented, put
|
|
||||||
// postRotate in a later vertex shader.
|
|
||||||
transformationMatrix.postRotate(outputRotationDegrees);
|
|
||||||
|
|
||||||
Format requestedOutputFormat =
|
|
||||||
new Format.Builder()
|
new Format.Builder()
|
||||||
.setWidth(outputWidth)
|
.setWidth(requestedEncoderWidth)
|
||||||
.setHeight(outputHeight)
|
.setHeight(requestedEncoderHeight)
|
||||||
.setRotationDegrees(0)
|
.setRotationDegrees(0)
|
||||||
.setSampleMimeType(
|
.setSampleMimeType(
|
||||||
transformationRequest.videoMimeType != null
|
transformationRequest.videoMimeType != null
|
||||||
? transformationRequest.videoMimeType
|
? transformationRequest.videoMimeType
|
||||||
: inputFormat.sampleMimeType)
|
: inputFormat.sampleMimeType)
|
||||||
.build();
|
.build();
|
||||||
encoder = encoderFactory.createForVideoEncoding(requestedOutputFormat, allowedOutputMimeTypes);
|
encoder = encoderFactory.createForVideoEncoding(requestedEncoderFormat, allowedOutputMimeTypes);
|
||||||
Format actualOutputFormat = encoder.getConfigurationFormat();
|
Format encoderSupportedFormat = encoder.getConfigurationFormat();
|
||||||
fallbackListener.onTransformationRequestFinalized(
|
fallbackListener.onTransformationRequestFinalized(
|
||||||
createFallbackTransformationRequest(
|
createFallbackTransformationRequest(
|
||||||
transformationRequest,
|
transformationRequest,
|
||||||
!swapEncodingDimensions,
|
!swapEncodingDimensions,
|
||||||
requestedOutputFormat,
|
requestedEncoderFormat,
|
||||||
actualOutputFormat));
|
encoderSupportedFormat));
|
||||||
|
|
||||||
if (transformationRequest.enableHdrEditing
|
if (transformationRequest.enableHdrEditing
|
||||||
|| inputFormat.height != actualOutputFormat.height
|
|| inputFormat.height != encoderSupportedFormat.height
|
||||||
|| inputFormat.width != actualOutputFormat.width
|
|| inputFormat.width != encoderSupportedFormat.width
|
||||||
|| !transformationMatrix.isIdentity()) {
|
|| !transformationMatrix.isIdentity()) {
|
||||||
frameEditor =
|
frameEditor =
|
||||||
FrameEditor.create(
|
FrameEditor.create(
|
||||||
context,
|
context,
|
||||||
actualOutputFormat.width,
|
encoderSupportedFormat.width,
|
||||||
actualOutputFormat.height,
|
encoderSupportedFormat.height,
|
||||||
inputFormat.pixelWidthHeightRatio,
|
inputFormat.pixelWidthHeightRatio,
|
||||||
transformationMatrix,
|
transformationMatrix,
|
||||||
/* outputSurface= */ encoder.getInputSurface(),
|
/* outputSurface= */ encoder.getInputSurface(),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user