FrameProcessor: Add a ScaleToFitFrameProcessor builder.

This allows us to input scale and rotation in an easier-to-use manner.

PiperOrigin-RevId: 436175982
This commit is contained in:
huangdarwin 2022-03-21 12:08:18 +00:00 committed by Ian Baker
parent ea67fbbb9a
commit 2a14e3c604
15 changed files with 236 additions and 179 deletions

View File

@ -50,8 +50,6 @@ public final class ConfigurationActivity extends AppCompatActivity {
public static final String AUDIO_MIME_TYPE = "audio_mime_type"; public static final String AUDIO_MIME_TYPE = "audio_mime_type";
public static final String VIDEO_MIME_TYPE = "video_mime_type"; public static final String VIDEO_MIME_TYPE = "video_mime_type";
public static final String RESOLUTION_HEIGHT = "resolution_height"; public static final String RESOLUTION_HEIGHT = "resolution_height";
public static final String TRANSLATE_X = "translate_x";
public static final String TRANSLATE_Y = "translate_y";
public static final String SCALE_X = "scale_x"; public static final String SCALE_X = "scale_x";
public static final String SCALE_Y = "scale_y"; public static final String SCALE_Y = "scale_y";
public static final String ROTATE_DEGREES = "rotate_degrees"; public static final String ROTATE_DEGREES = "rotate_degrees";
@ -81,7 +79,6 @@ public final class ConfigurationActivity extends AppCompatActivity {
private @MonotonicNonNull Spinner audioMimeSpinner; private @MonotonicNonNull Spinner audioMimeSpinner;
private @MonotonicNonNull Spinner videoMimeSpinner; private @MonotonicNonNull Spinner videoMimeSpinner;
private @MonotonicNonNull Spinner resolutionHeightSpinner; private @MonotonicNonNull Spinner resolutionHeightSpinner;
private @MonotonicNonNull Spinner translateSpinner;
private @MonotonicNonNull Spinner scaleSpinner; private @MonotonicNonNull Spinner scaleSpinner;
private @MonotonicNonNull Spinner rotateSpinner; private @MonotonicNonNull Spinner rotateSpinner;
private @MonotonicNonNull CheckBox enableFallbackCheckBox; private @MonotonicNonNull CheckBox enableFallbackCheckBox;
@ -136,14 +133,6 @@ public final class ConfigurationActivity extends AppCompatActivity {
resolutionHeightAdapter.addAll( resolutionHeightAdapter.addAll(
SAME_AS_INPUT_OPTION, "144", "240", "360", "480", "720", "1080", "1440", "2160"); SAME_AS_INPUT_OPTION, "144", "240", "360", "480", "720", "1080", "1440", "2160");
ArrayAdapter<String> translateAdapter =
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
translateAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
translateSpinner = findViewById(R.id.translate_spinner);
translateSpinner.setAdapter(translateAdapter);
translateAdapter.addAll(
SAME_AS_INPUT_OPTION, "-.1, -.1", "0, 0", ".5, 0", "0, .5", "1, 1", "1.9, 0", "0, 1.9");
ArrayAdapter<String> scaleAdapter = ArrayAdapter<String> scaleAdapter =
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
scaleAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); scaleAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
@ -185,7 +174,6 @@ public final class ConfigurationActivity extends AppCompatActivity {
"audioMimeSpinner", "audioMimeSpinner",
"videoMimeSpinner", "videoMimeSpinner",
"resolutionHeightSpinner", "resolutionHeightSpinner",
"translateSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableFallbackCheckBox", "enableFallbackCheckBox",
@ -209,13 +197,6 @@ public final class ConfigurationActivity extends AppCompatActivity {
if (!SAME_AS_INPUT_OPTION.equals(selectedResolutionHeight)) { if (!SAME_AS_INPUT_OPTION.equals(selectedResolutionHeight)) {
bundle.putInt(RESOLUTION_HEIGHT, Integer.parseInt(selectedResolutionHeight)); bundle.putInt(RESOLUTION_HEIGHT, Integer.parseInt(selectedResolutionHeight));
} }
String selectedTranslate = String.valueOf(translateSpinner.getSelectedItem());
if (!SAME_AS_INPUT_OPTION.equals(selectedTranslate)) {
List<String> translateXY = Arrays.asList(selectedTranslate.split(", "));
checkState(translateXY.size() == 2);
bundle.putFloat(TRANSLATE_X, Float.parseFloat(translateXY.get(0)));
bundle.putFloat(TRANSLATE_Y, Float.parseFloat(translateXY.get(1)));
}
String selectedScale = String.valueOf(scaleSpinner.getSelectedItem()); String selectedScale = String.valueOf(scaleSpinner.getSelectedItem());
if (!SAME_AS_INPUT_OPTION.equals(selectedScale)) { if (!SAME_AS_INPUT_OPTION.equals(selectedScale)) {
List<String> scaleXY = Arrays.asList(selectedScale.split(", ")); List<String> scaleXY = Arrays.asList(selectedScale.split(", "));
@ -258,7 +239,6 @@ public final class ConfigurationActivity extends AppCompatActivity {
"audioMimeSpinner", "audioMimeSpinner",
"videoMimeSpinner", "videoMimeSpinner",
"resolutionHeightSpinner", "resolutionHeightSpinner",
"translateSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableHdrEditingCheckBox" "enableHdrEditingCheckBox"
@ -277,7 +257,6 @@ public final class ConfigurationActivity extends AppCompatActivity {
"audioMimeSpinner", "audioMimeSpinner",
"videoMimeSpinner", "videoMimeSpinner",
"resolutionHeightSpinner", "resolutionHeightSpinner",
"translateSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableHdrEditingCheckBox" "enableHdrEditingCheckBox"
@ -295,7 +274,6 @@ public final class ConfigurationActivity extends AppCompatActivity {
"audioMimeSpinner", "audioMimeSpinner",
"videoMimeSpinner", "videoMimeSpinner",
"resolutionHeightSpinner", "resolutionHeightSpinner",
"translateSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableHdrEditingCheckBox" "enableHdrEditingCheckBox"
@ -304,7 +282,6 @@ public final class ConfigurationActivity extends AppCompatActivity {
audioMimeSpinner.setEnabled(isAudioEnabled); audioMimeSpinner.setEnabled(isAudioEnabled);
videoMimeSpinner.setEnabled(isVideoEnabled); videoMimeSpinner.setEnabled(isVideoEnabled);
resolutionHeightSpinner.setEnabled(isVideoEnabled); resolutionHeightSpinner.setEnabled(isVideoEnabled);
translateSpinner.setEnabled(isVideoEnabled);
scaleSpinner.setEnabled(isVideoEnabled); scaleSpinner.setEnabled(isVideoEnabled);
rotateSpinner.setEnabled(isVideoEnabled); rotateSpinner.setEnabled(isVideoEnabled);
enableHdrEditingCheckBox.setEnabled(isVideoEnabled); enableHdrEditingCheckBox.setEnabled(isVideoEnabled);
@ -312,7 +289,6 @@ public final class ConfigurationActivity extends AppCompatActivity {
findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled); findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled);
findViewById(R.id.video_mime_text_view).setEnabled(isVideoEnabled); findViewById(R.id.video_mime_text_view).setEnabled(isVideoEnabled);
findViewById(R.id.resolution_height_text_view).setEnabled(isVideoEnabled); findViewById(R.id.resolution_height_text_view).setEnabled(isVideoEnabled);
findViewById(R.id.translate).setEnabled(isVideoEnabled);
findViewById(R.id.scale).setEnabled(isVideoEnabled); findViewById(R.id.scale).setEnabled(isVideoEnabled);
findViewById(R.id.rotate).setEnabled(isVideoEnabled); findViewById(R.id.rotate).setEnabled(isVideoEnabled);
findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled); findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled);

View File

@ -21,7 +21,6 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Matrix;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
@ -217,10 +216,15 @@ public final class TransformerActivity extends AppCompatActivity {
if (resolutionHeight != C.LENGTH_UNSET) { if (resolutionHeight != C.LENGTH_UNSET) {
requestBuilder.setResolution(resolutionHeight); requestBuilder.setResolution(resolutionHeight);
} }
Matrix transformationMatrix = getTransformationMatrix(bundle);
if (!transformationMatrix.isIdentity()) { float scaleX = bundle.getFloat(ConfigurationActivity.SCALE_X, /* defaultValue= */ 1);
requestBuilder.setTransformationMatrix(transformationMatrix); float scaleY = bundle.getFloat(ConfigurationActivity.SCALE_Y, /* defaultValue= */ 1);
} requestBuilder.setScale(scaleX, scaleY);
float rotateDegrees =
bundle.getFloat(ConfigurationActivity.ROTATE_DEGREES, /* defaultValue= */ 0);
requestBuilder.setRotationDegrees(rotateDegrees);
requestBuilder.experimental_setEnableHdrEditing( requestBuilder.experimental_setEnableHdrEditing(
bundle.getBoolean(ConfigurationActivity.ENABLE_HDR_EDITING)); bundle.getBoolean(ConfigurationActivity.ENABLE_HDR_EDITING));
transformerBuilder transformerBuilder
@ -251,27 +255,6 @@ public final class TransformerActivity extends AppCompatActivity {
.build(); .build();
} }
private static Matrix getTransformationMatrix(Bundle bundle) {
Matrix transformationMatrix = new Matrix();
float translateX = bundle.getFloat(ConfigurationActivity.TRANSLATE_X, /* defaultValue= */ 0);
float translateY = bundle.getFloat(ConfigurationActivity.TRANSLATE_Y, /* defaultValue= */ 0);
// TODO(b/201293185): Implement an AdvancedFrameEditor to handle translation, as the current
// transformationMatrix is automatically adjusted to focus on the original pixels and
// effectively undo translations.
transformationMatrix.postTranslate(translateX, translateY);
float scaleX = bundle.getFloat(ConfigurationActivity.SCALE_X, /* defaultValue= */ 1);
float scaleY = bundle.getFloat(ConfigurationActivity.SCALE_Y, /* defaultValue= */ 1);
transformationMatrix.postScale(scaleX, scaleY);
float rotateDegrees =
bundle.getFloat(ConfigurationActivity.ROTATE_DEGREES, /* defaultValue= */ 0);
transformationMatrix.postRotate(rotateDegrees);
return transformationMatrix;
}
@RequiresNonNull({ @RequiresNonNull({
"informationTextView", "informationTextView",
"progressViewGroup", "progressViewGroup",

View File

@ -137,17 +137,6 @@
android:layout_gravity="right|center_vertical" android:layout_gravity="right|center_vertical"
android:gravity="right" /> android:gravity="right" />
</TableRow> </TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/translate"
android:text="@string/translate"/>
<Spinner
android:id="@+id/translate_spinner"
android:layout_gravity="right|center_vertical"
android:gravity="right" />
</TableRow>
<TableRow <TableRow
android:layout_weight="1" android:layout_weight="1"
android:gravity="center_vertical" > android:gravity="center_vertical" >

View File

@ -24,7 +24,6 @@
<string name="audio_mime" translatable="false">Output audio MIME type</string> <string name="audio_mime" translatable="false">Output audio MIME type</string>
<string name="video_mime" translatable="false">Output video MIME type</string> <string name="video_mime" translatable="false">Output video MIME type</string>
<string name="resolution_height" translatable="false">Output video resolution</string> <string name="resolution_height" translatable="false">Output video resolution</string>
<string name="translate" translatable="false">Translate video</string>
<string name="scale" translatable="false">Scale video</string> <string name="scale" translatable="false">Scale video</string>
<string name="rotate" translatable="false">Rotate video (degrees)</string> <string name="rotate" translatable="false">Rotate video (degrees)</string>
<string name="enable_fallback" translatable="false">Enable fallback</string> <string name="enable_fallback" translatable="false">Enable fallback</string>

View File

@ -38,7 +38,6 @@ import android.media.MediaExtractor;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.util.Size; import android.util.Size;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -175,10 +174,8 @@ public final class FrameEditorDataProcessingTest {
// TODO(b/213190310): After creating a Presentation class, move VideoSamplePipeline // TODO(b/213190310): After creating a Presentation class, move VideoSamplePipeline
// resolution-based adjustments (ex. in cl/419619743) to that Presentation class, so we can // resolution-based adjustments (ex. in cl/419619743) to that Presentation class, so we can
// test that rotation doesn't distort the image. // test that rotation doesn't distort the image.
Matrix identityMatrix = new Matrix();
GlFrameProcessor glFrameProcessor = GlFrameProcessor glFrameProcessor =
new ScaleToFitFrameProcessor( new ScaleToFitFrameProcessor.Builder(getApplicationContext()).setResolution(480).build();
getApplicationContext(), identityMatrix, /* requestedHeight= */ 480);
setUpAndPrepareFirstFrame(glFrameProcessor); setUpAndPrepareFirstFrame(glFrameProcessor);
Bitmap expectedBitmap = Bitmap expectedBitmap =
BitmapTestUtil.readBitmap(REQUEST_OUTPUT_HEIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING); BitmapTestUtil.readBitmap(REQUEST_OUTPUT_HEIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING);
@ -200,11 +197,10 @@ public final class FrameEditorDataProcessingTest {
// TODO(b/213190310): After creating a Presentation class, move VideoSamplePipeline // TODO(b/213190310): After creating a Presentation class, move VideoSamplePipeline
// resolution-based adjustments (ex. in cl/419619743) to that Presentation class, so we can // resolution-based adjustments (ex. in cl/419619743) to that Presentation class, so we can
// test that rotation doesn't distort the image. // test that rotation doesn't distort the image.
Matrix rotate45Matrix = new Matrix();
rotate45Matrix.postRotate(/* degrees= */ 45);
GlFrameProcessor glFrameProcessor = GlFrameProcessor glFrameProcessor =
new ScaleToFitFrameProcessor( new ScaleToFitFrameProcessor.Builder(getApplicationContext())
getApplicationContext(), rotate45Matrix, /* requestedHeight= */ C.LENGTH_UNSET); .setRotationDegrees(45)
.build();
setUpAndPrepareFirstFrame(glFrameProcessor); setUpAndPrepareFirstFrame(glFrameProcessor);
Bitmap expectedBitmap = Bitmap expectedBitmap =
BitmapTestUtil.readBitmap(ROTATE45_SCALE_TO_FIT_EXPECTED_OUTPUT_PNG_ASSET_STRING); BitmapTestUtil.readBitmap(ROTATE45_SCALE_TO_FIT_EXPECTED_OUTPUT_PNG_ASSET_STRING);

View File

@ -19,7 +19,6 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.content.Context; import android.content.Context;
import android.graphics.Matrix;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -67,16 +66,12 @@ public class TransformerEndToEndTest {
@Test @Test
public void videoEditing_completesWithConsistentFrameCount() throws Exception { public void videoEditing_completesWithConsistentFrameCount() throws Exception {
Context context = ApplicationProvider.getApplicationContext(); Context context = ApplicationProvider.getApplicationContext();
Matrix transformationMatrix = new Matrix();
transformationMatrix.postTranslate(/* dx= */ .2f, /* dy= */ .1f);
FrameCountingMuxer.Factory muxerFactory = FrameCountingMuxer.Factory muxerFactory =
new FrameCountingMuxer.Factory(new FrameworkMuxer.Factory()); new FrameCountingMuxer.Factory(new FrameworkMuxer.Factory());
Transformer transformer = Transformer transformer =
new Transformer.Builder(context) new Transformer.Builder(context)
.setTransformationRequest( .setTransformationRequest(
new TransformationRequest.Builder() new TransformationRequest.Builder().setResolution(480).build())
.setTransformationMatrix(transformationMatrix)
.build())
.setMuxerFactory(muxerFactory) .setMuxerFactory(muxerFactory)
.setEncoderFactory( .setEncoderFactory(
new DefaultEncoderFactory(EncoderSelector.DEFAULT, /* enableFallback= */ false)) new DefaultEncoderFactory(EncoderSelector.DEFAULT, /* enableFallback= */ false))

View File

@ -19,7 +19,6 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
import static com.google.common.truth.Truth.assertWithMessage; import static com.google.common.truth.Truth.assertWithMessage;
import android.content.Context; import android.content.Context;
import android.graphics.Matrix;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.transformer.AndroidTestUtil; import androidx.media3.transformer.AndroidTestUtil;
import androidx.media3.transformer.TransformationRequest; import androidx.media3.transformer.TransformationRequest;
@ -41,13 +40,11 @@ public final class RepeatedTranscodeTransformationTest {
@Test @Test
public void repeatedTranscode_givesConsistentLengthOutput() throws Exception { public void repeatedTranscode_givesConsistentLengthOutput() throws Exception {
Context context = ApplicationProvider.getApplicationContext(); Context context = ApplicationProvider.getApplicationContext();
Matrix transformationMatrix = new Matrix();
transformationMatrix.postTranslate(/* dx= */ 0.1f, /* dy= */ 0.1f);
Transformer transformer = Transformer transformer =
new Transformer.Builder(context) new Transformer.Builder(context)
.setTransformationRequest( .setTransformationRequest(
new TransformationRequest.Builder() new TransformationRequest.Builder()
.setTransformationMatrix(transformationMatrix) .setRotationDegrees(45)
// Video MIME type is H264. // Video MIME type is H264.
.setAudioMimeType(MimeTypes.AUDIO_AAC) .setAudioMimeType(MimeTypes.AUDIO_AAC)
.build()) .build())
@ -74,15 +71,13 @@ public final class RepeatedTranscodeTransformationTest {
@Test @Test
public void repeatedTranscodeNoAudio_givesConsistentLengthOutput() throws Exception { public void repeatedTranscodeNoAudio_givesConsistentLengthOutput() throws Exception {
Context context = ApplicationProvider.getApplicationContext(); Context context = ApplicationProvider.getApplicationContext();
Matrix transformationMatrix = new Matrix();
transformationMatrix.postTranslate(/* dx= */ 0.1f, /* dy= */ 0.1f);
Transformer transformer = Transformer transformer =
new Transformer.Builder(context) new Transformer.Builder(context)
.setRemoveAudio(true) .setRemoveAudio(true)
.setTransformationRequest( .setTransformationRequest(
new TransformationRequest.Builder() new TransformationRequest.Builder()
// Video MIME type is H264. // Video MIME type is H264.
.setTransformationMatrix(transformationMatrix) .setRotationDegrees(45)
.build()) .build())
.build(); .build();

View File

@ -18,7 +18,6 @@ package androidx.media3.transformer.mh;
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING;
import android.content.Context; import android.content.Context;
import android.graphics.Matrix;
import androidx.media3.transformer.TransformationRequest; import androidx.media3.transformer.TransformationRequest;
import androidx.media3.transformer.Transformer; import androidx.media3.transformer.Transformer;
import androidx.media3.transformer.TransformerAndroidTestRunner; import androidx.media3.transformer.TransformerAndroidTestRunner;
@ -27,26 +26,21 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
/** {@link Transformer} instrumentation test for setting a transformation matrix. */ /** {@link Transformer} instrumentation test for applying a frame edit. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class SetTransformationMatrixTransformationTest { public class SetFrameEditTransformationTest {
@Test @Test
public void setTransformationMatrixTransform() throws Exception { public void setFrameEditTransform() throws Exception {
Context context = ApplicationProvider.getApplicationContext(); Context context = ApplicationProvider.getApplicationContext();
Matrix transformationMatrix = new Matrix();
transformationMatrix.postTranslate(/* dx= */ .2f, /* dy= */ .1f);
Transformer transformer = Transformer transformer =
new Transformer.Builder(context) new Transformer.Builder(context)
.setTransformationRequest( .setTransformationRequest(
new TransformationRequest.Builder() new TransformationRequest.Builder().setRotationDegrees(45).build())
.setTransformationMatrix(transformationMatrix)
.build())
.build(); .build();
new TransformerAndroidTestRunner.Builder(context, transformer) new TransformerAndroidTestRunner.Builder(context, transformer)
.build() .build()
.run( .run(
/* testId= */ "setTransformationMatrixTransform", /* testId= */ "SetFrameEditTransform", MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING);
MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING);
} }
} }

View File

@ -38,6 +38,81 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
*/ */
/* package */ final class ScaleToFitFrameProcessor implements GlFrameProcessor { /* package */ final class ScaleToFitFrameProcessor implements GlFrameProcessor {
/** A builder for {@link ScaleToFitFrameProcessor} instances. */
public static final class Builder {
// Mandatory field.
Context context;
// Optional field.
private int outputHeight;
private float scaleX;
private float scaleY;
private float rotationDegrees;
/**
* Creates a builder with default values.
*
* @param context The {@link Context}.
*/
public Builder(Context context) {
this.context = context;
outputHeight = C.LENGTH_UNSET;
scaleX = 1;
scaleY = 1;
rotationDegrees = 0;
}
/**
* Sets the x and y axis scaling factors to apply to each frame's width and height.
*
* <p>The values default to 1, which corresponds to not scaling along both axes.
*
* @param scaleX The multiplier by which the frame will scale horizontally, along the x-axis.
* @param scaleY The multiplier by which the frame will scale vertically, along the y-axis.
* @return This builder.
*/
public Builder setScale(float scaleX, float scaleY) {
this.scaleX = scaleX;
this.scaleY = scaleY;
return this;
}
/**
* Sets the counterclockwise rotation degrees.
*
* <p>The default value, 0, corresponds to not applying any rotation.
*
* @param rotationDegrees The counterclockwise rotation, in degrees.
* @return This builder.
*/
public Builder setRotationDegrees(float rotationDegrees) {
this.rotationDegrees = rotationDegrees;
return this;
}
/**
* Sets the output resolution using the output height.
*
* <p>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.
*
* <p>For example, a 1920x1440 frame can be scaled to 640x480 by calling setResolution(480).
*
* @param outputHeight The output height of the displayed frame, in pixels.
* @return This builder.
*/
public Builder setResolution(int outputHeight) {
this.outputHeight = outputHeight;
return this;
}
public ScaleToFitFrameProcessor build() {
return new ScaleToFitFrameProcessor(context, scaleX, scaleY, rotationDegrees, outputHeight);
}
}
static { static {
GlUtil.glAssertionsEnabled = true; GlUtil.glAssertionsEnabled = true;
} }
@ -58,15 +133,18 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* Creates a new instance. * Creates a new instance.
* *
* @param context The {@link Context}. * @param context The {@link Context}.
* @param transformationMatrix The transformation matrix to apply to each frame. * @param scaleX The multiplier by which the frame will scale horizontally, along the x-axis.
* @param scaleY The multiplier by which the frame will scale vertically, along the y-axis.
* @param rotationDegrees How much to rotate the frame counterclockwise, in degrees.
* @param requestedHeight The height of the output frame, in pixels. * @param requestedHeight The height of the output frame, in pixels.
*/ */
public ScaleToFitFrameProcessor( private ScaleToFitFrameProcessor(
Context context, Matrix transformationMatrix, int requestedHeight) { Context context, float scaleX, float scaleY, float rotationDegrees, int requestedHeight) {
// TODO(b/201293185): Replace transformationMatrix parameter with scale and rotation.
this.context = context; this.context = context;
this.transformationMatrix = new Matrix(transformationMatrix); this.transformationMatrix = new Matrix();
this.transformationMatrix.postScale(scaleX, scaleY);
this.transformationMatrix.postRotate(rotationDegrees);
this.requestedHeight = requestedHeight; this.requestedHeight = requestedHeight;
inputWidth = C.LENGTH_UNSET; inputWidth = C.LENGTH_UNSET;

View File

@ -18,7 +18,6 @@ package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkArgument;
import android.graphics.Matrix;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
@ -34,8 +33,10 @@ 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 Matrix transformationMatrix;
private boolean flattenForSlowMotion; private boolean flattenForSlowMotion;
private float scaleX;
private float scaleY;
private float rotationDegrees;
private int outputHeight; private int outputHeight;
@Nullable private String audioMimeType; @Nullable private String audioMimeType;
@Nullable private String videoMimeType; @Nullable private String videoMimeType;
@ -48,43 +49,23 @@ public final class TransformationRequest {
* {@link TransformationRequest}. * {@link TransformationRequest}.
*/ */
public Builder() { public Builder() {
transformationMatrix = new Matrix(); scaleX = 1;
scaleY = 1;
rotationDegrees = 0;
outputHeight = C.LENGTH_UNSET; outputHeight = C.LENGTH_UNSET;
} }
private Builder(TransformationRequest transformationRequest) { private Builder(TransformationRequest transformationRequest) {
this.transformationMatrix = new Matrix(transformationRequest.transformationMatrix);
this.flattenForSlowMotion = transformationRequest.flattenForSlowMotion; this.flattenForSlowMotion = transformationRequest.flattenForSlowMotion;
this.scaleX = transformationRequest.scaleX;
this.scaleY = transformationRequest.scaleY;
this.rotationDegrees = transformationRequest.rotationDegrees;
this.outputHeight = transformationRequest.outputHeight; this.outputHeight = transformationRequest.outputHeight;
this.audioMimeType = transformationRequest.audioMimeType; this.audioMimeType = transformationRequest.audioMimeType;
this.videoMimeType = transformationRequest.videoMimeType; this.videoMimeType = transformationRequest.videoMimeType;
this.enableHdrEditing = transformationRequest.enableHdrEditing; this.enableHdrEditing = transformationRequest.enableHdrEditing;
} }
/**
* Sets the transformation matrix.
*
* <p>The default value is to apply no change.
*
* <p>This can be used to perform operations supported by {@link Matrix}, like scaling and
* rotating the video.
*
* <p>The video dimensions will be on the x axis, from -aspectRatio to aspectRatio, and on the y
* axis, from -1 to 1.
*
* <p>For now, resolution will not be affected by this method.
*
* @param transformationMatrix The transformation to apply to video frames.
* @return This builder.
*/
public Builder setTransformationMatrix(Matrix transformationMatrix) {
// TODO(b/201293185): Implement an AdvancedFrameEditor to handle translation, as the current
// transformationMatrix is automatically adjusted to focus on the original pixels and
// effectively undo translations.
this.transformationMatrix = new Matrix(transformationMatrix);
return this;
}
/** /**
* Sets whether the input should be flattened for media containing slow motion markers. * Sets whether the input should be flattened for media containing slow motion markers.
* *
@ -116,6 +97,36 @@ public final class TransformationRequest {
return this; return this;
} }
/**
* Sets the x and y axis scaling factors to apply to each frame's width and height, stretching
* the video along these axes appropriately.
*
* <p>The values default to 1, which corresponds to not scaling along both axes.
*
* @param scaleX The multiplier by which the frame will scale horizontally, along the x-axis.
* @param scaleY The multiplier by which the frame will scale vertically, along the y-axis.
* @return This builder.
*/
public Builder setScale(float scaleX, float scaleY) {
this.scaleX = scaleX;
this.scaleY = scaleY;
return this;
}
/**
* Sets the rotation, in degrees, counterclockwise, to apply to each frame, automatically
* adjusting the frame's width and height to preserve all input pixels.
*
* <p>The default value, 0, corresponds to not applying any rotation.
*
* @param rotationDegrees The counterclockwise rotation, in degrees.
* @return This builder.
*/
public Builder setRotationDegrees(float rotationDegrees) {
this.rotationDegrees = rotationDegrees;
return this;
}
/** /**
* Sets the output resolution using the output height. * Sets the output resolution using the output height.
* *
@ -205,8 +216,10 @@ public final class TransformationRequest {
/** Builds a {@link TransformationRequest} instance. */ /** Builds a {@link TransformationRequest} instance. */
public TransformationRequest build() { public TransformationRequest build() {
return new TransformationRequest( return new TransformationRequest(
transformationMatrix,
flattenForSlowMotion, flattenForSlowMotion,
scaleX,
scaleY,
rotationDegrees,
outputHeight, outputHeight,
audioMimeType, audioMimeType,
videoMimeType, videoMimeType,
@ -214,18 +227,32 @@ public final class TransformationRequest {
} }
} }
/**
* A {@link Matrix transformation matrix} to apply to video frames.
*
* @see Builder#setTransformationMatrix(Matrix)
*/
public final Matrix transformationMatrix;
/** /**
* Whether the input should be flattened for media containing slow motion markers. * Whether the input should be flattened for media containing slow motion markers.
* *
* @see Builder#setFlattenForSlowMotion(boolean) * @see Builder#setFlattenForSlowMotion(boolean)
*/ */
public final boolean flattenForSlowMotion; public final boolean flattenForSlowMotion;
/**
* The requested scale factor, on the x-axis, of the output video, or 1 if inferred from the
* input.
*
* @see Builder#setScale(float, float)
*/
public final float scaleX;
/**
* The requested scale factor, on the y-axis, of the output video, or 1 if inferred from the
* input.
*
* @see Builder#setScale(float, float)
*/
public final float scaleY;
/**
* The requested rotation, in degrees, of the output video, or 0 if inferred from the input.
*
* @see Builder#setRotationDegrees(float)
*/
public final float rotationDegrees;
/** /**
* The requested height of the output video, or {@link C#LENGTH_UNSET} if inferred from the input. * The requested height of the output video, or {@link C#LENGTH_UNSET} if inferred from the input.
* *
@ -254,14 +281,18 @@ public final class TransformationRequest {
public final boolean enableHdrEditing; public final boolean enableHdrEditing;
private TransformationRequest( private TransformationRequest(
Matrix transformationMatrix,
boolean flattenForSlowMotion, boolean flattenForSlowMotion,
float scaleX,
float scaleY,
float rotationDegrees,
int outputHeight, int outputHeight,
@Nullable String audioMimeType, @Nullable String audioMimeType,
@Nullable String videoMimeType, @Nullable String videoMimeType,
boolean enableHdrEditing) { boolean enableHdrEditing) {
this.transformationMatrix = transformationMatrix;
this.flattenForSlowMotion = flattenForSlowMotion; this.flattenForSlowMotion = flattenForSlowMotion;
this.scaleX = scaleX;
this.scaleY = scaleY;
this.rotationDegrees = rotationDegrees;
this.outputHeight = outputHeight; this.outputHeight = outputHeight;
this.audioMimeType = audioMimeType; this.audioMimeType = audioMimeType;
this.videoMimeType = videoMimeType; this.videoMimeType = videoMimeType;
@ -277,8 +308,10 @@ public final class TransformationRequest {
return false; return false;
} }
TransformationRequest that = (TransformationRequest) o; TransformationRequest that = (TransformationRequest) o;
return transformationMatrix.equals(that.transformationMatrix) return flattenForSlowMotion == that.flattenForSlowMotion
&& flattenForSlowMotion == that.flattenForSlowMotion && scaleX == that.scaleX
&& scaleY == that.scaleY
&& rotationDegrees == that.rotationDegrees
&& outputHeight == that.outputHeight && outputHeight == that.outputHeight
&& Util.areEqual(audioMimeType, that.audioMimeType) && Util.areEqual(audioMimeType, that.audioMimeType)
&& Util.areEqual(videoMimeType, that.videoMimeType) && Util.areEqual(videoMimeType, that.videoMimeType)
@ -287,8 +320,10 @@ public final class TransformationRequest {
@Override @Override
public int hashCode() { public int hashCode() {
int result = transformationMatrix.hashCode(); int result = (flattenForSlowMotion ? 1 : 0);
result = 31 * result + (flattenForSlowMotion ? 1 : 0); result = 31 * result + Float.floatToIntBits(scaleX);
result = 31 * result + Float.floatToIntBits(scaleY);
result = 31 * result + Float.floatToIntBits(rotationDegrees);
result = 31 * result + outputHeight; result = 31 * result + outputHeight;
result = 31 * result + (audioMimeType != null ? audioMimeType.hashCode() : 0); result = 31 * result + (audioMimeType != null ? audioMimeType.hashCode() : 0);
result = 31 * result + (videoMimeType != null ? videoMimeType.hashCode() : 0); result = 31 * result + (videoMimeType != null ? videoMimeType.hashCode() : 0);

View File

@ -168,6 +168,10 @@ public final class Transformer {
/** /**
* Sets the {@link TransformationRequest} which configures the editing and transcoding options. * Sets the {@link TransformationRequest} which configures the editing and transcoding options.
* *
* <p>Actual applied values may differ, per device capabilities. {@link
* Listener#onFallbackApplied(MediaItem, TransformationRequest, TransformationRequest)} will be
* invoked with the actual applied values.
*
* @param transformationRequest The {@link TransformationRequest}. * @param transformationRequest The {@link TransformationRequest}.
* @return This builder. * @return This builder.
*/ */

View File

@ -113,14 +113,17 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
&& !muxerWrapper.supportsSampleMimeType(inputFormat.sampleMimeType)) { && !muxerWrapper.supportsSampleMimeType(inputFormat.sampleMimeType)) {
return false; return false;
} }
if (transformationRequest.outputHeight != C.LENGTH_UNSET if (transformationRequest.rotationDegrees != 0f) {
&& transformationRequest.outputHeight != inputFormat.height) {
return false; return false;
} }
if (!transformationRequest.transformationMatrix.isIdentity()) { if (transformationRequest.scaleX != 1f) {
// TODO(b/201293185, b/214010296): Move FrameProcessor transformationMatrix calculation / return false;
// adjustments out of the VideoTranscodingSamplePipeline, so that we can skip transcoding when }
// adjustments result in identity matrices. if (transformationRequest.scaleY != 1f) {
return false;
}
if (transformationRequest.outputHeight != C.LENGTH_UNSET
&& transformationRequest.outputHeight != inputFormat.height) {
return false; return false;
} }
return true; return true;

View File

@ -69,10 +69,11 @@ import org.checkerframework.dataflow.qual.Pure;
(inputFormat.rotationDegrees % 180 == 0) ? inputFormat.height : inputFormat.width; (inputFormat.rotationDegrees % 180 == 0) ? inputFormat.height : inputFormat.width;
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
new ScaleToFitFrameProcessor( new ScaleToFitFrameProcessor.Builder(context)
context, .setScale(transformationRequest.scaleX, transformationRequest.scaleY)
transformationRequest.transformationMatrix, .setRotationDegrees(transformationRequest.rotationDegrees)
transformationRequest.outputHeight); .setResolution(transformationRequest.outputHeight)
.build();
Size requestedEncoderDimensions = Size requestedEncoderDimensions =
scaleToFitFrameProcessor.configureOutputSize(decodedWidth, decodedHeight); scaleToFitFrameProcessor.configureOutputSize(decodedWidth, decodedHeight);
outputRotationDegrees = scaleToFitFrameProcessor.getOutputRotationDegrees(); outputRotationDegrees = scaleToFitFrameProcessor.getOutputRotationDegrees();

View File

@ -19,9 +19,7 @@ import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import android.graphics.Matrix;
import android.util.Size; import android.util.Size;
import androidx.media3.common.C;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -37,11 +35,10 @@ public final class ScaleToFitFrameProcessorTest {
@Test @Test
public void configureOutputDimensions_noEdits_producesExpectedOutput() { public void configureOutputDimensions_noEdits_producesExpectedOutput() {
Matrix identityMatrix = new Matrix();
int inputWidth = 200; int inputWidth = 200;
int inputHeight = 150; int inputHeight = 150;
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
new ScaleToFitFrameProcessor(getApplicationContext(), identityMatrix, C.LENGTH_UNSET); new ScaleToFitFrameProcessor.Builder(getApplicationContext()).build();
Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight);
@ -53,9 +50,8 @@ public final class ScaleToFitFrameProcessorTest {
@Test @Test
public void initializeBeforeConfigure_throwsIllegalStateException() { public void initializeBeforeConfigure_throwsIllegalStateException() {
Matrix identityMatrix = new Matrix();
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
new ScaleToFitFrameProcessor(getApplicationContext(), identityMatrix, C.LENGTH_UNSET); new ScaleToFitFrameProcessor.Builder(getApplicationContext()).build();
// configureOutputDimensions not called before initialize. // configureOutputDimensions not called before initialize.
assertThrows( assertThrows(
@ -65,9 +61,8 @@ public final class ScaleToFitFrameProcessorTest {
@Test @Test
public void getOutputRotationDegreesBeforeConfigure_throwsIllegalStateException() { public void getOutputRotationDegreesBeforeConfigure_throwsIllegalStateException() {
Matrix identityMatrix = new Matrix();
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
new ScaleToFitFrameProcessor(getApplicationContext(), identityMatrix, C.LENGTH_UNSET); new ScaleToFitFrameProcessor.Builder(getApplicationContext()).build();
// configureOutputDimensions not called before initialize. // configureOutputDimensions not called before initialize.
assertThrows(IllegalStateException.class, scaleToFitFrameProcessor::getOutputRotationDegrees); assertThrows(IllegalStateException.class, scaleToFitFrameProcessor::getOutputRotationDegrees);
@ -75,12 +70,12 @@ public final class ScaleToFitFrameProcessorTest {
@Test @Test
public void configureOutputDimensions_scaleNarrow_producesExpectedOutput() { public void configureOutputDimensions_scaleNarrow_producesExpectedOutput() {
Matrix scaleNarrowMatrix = new Matrix();
scaleNarrowMatrix.postScale(/* sx= */ .5f, /* sy= */ 1.0f);
int inputWidth = 200; int inputWidth = 200;
int inputHeight = 150; int inputHeight = 150;
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
new ScaleToFitFrameProcessor(getApplicationContext(), scaleNarrowMatrix, C.LENGTH_UNSET); new ScaleToFitFrameProcessor.Builder(getApplicationContext())
.setScale(/* scaleX= */ .5f, /* scaleY= */ 1f)
.build();
Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight);
@ -92,12 +87,12 @@ public final class ScaleToFitFrameProcessorTest {
@Test @Test
public void configureOutputDimensions_scaleWide_producesExpectedOutput() { public void configureOutputDimensions_scaleWide_producesExpectedOutput() {
Matrix scaleNarrowMatrix = new Matrix();
scaleNarrowMatrix.postScale(/* sx= */ 2f, /* sy= */ 1.0f);
int inputWidth = 200; int inputWidth = 200;
int inputHeight = 150; int inputHeight = 150;
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
new ScaleToFitFrameProcessor(getApplicationContext(), scaleNarrowMatrix, C.LENGTH_UNSET); new ScaleToFitFrameProcessor.Builder(getApplicationContext())
.setScale(/* scaleX= */ 2f, /* scaleY= */ 1f)
.build();
Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight);
@ -108,13 +103,30 @@ public final class ScaleToFitFrameProcessorTest {
} }
@Test @Test
public void configureOutputDimensions_rotate90_producesExpectedOutput() { public void configureOutputDimensions_scaleTall_producesExpectedOutput() {
Matrix rotate90Matrix = new Matrix();
rotate90Matrix.postRotate(/* degrees= */ 90);
int inputWidth = 200; int inputWidth = 200;
int inputHeight = 150; int inputHeight = 150;
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
new ScaleToFitFrameProcessor(getApplicationContext(), rotate90Matrix, C.LENGTH_UNSET); new ScaleToFitFrameProcessor.Builder(getApplicationContext())
.setScale(/* scaleX= */ 1f, /* scaleY= */ 2f)
.build();
Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight);
assertThat(scaleToFitFrameProcessor.getOutputRotationDegrees()).isEqualTo(90);
assertThat(scaleToFitFrameProcessor.shouldProcess()).isTrue();
assertThat(outputSize.getWidth()).isEqualTo(inputHeight * 2);
assertThat(outputSize.getHeight()).isEqualTo(inputWidth);
}
@Test
public void configureOutputDimensions_rotate90_producesExpectedOutput() {
int inputWidth = 200;
int inputHeight = 150;
ScaleToFitFrameProcessor scaleToFitFrameProcessor =
new ScaleToFitFrameProcessor.Builder(getApplicationContext())
.setRotationDegrees(90)
.build();
Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight);
@ -126,12 +138,12 @@ public final class ScaleToFitFrameProcessorTest {
@Test @Test
public void configureOutputDimensions_rotate45_producesExpectedOutput() { public void configureOutputDimensions_rotate45_producesExpectedOutput() {
Matrix rotate45Matrix = new Matrix();
rotate45Matrix.postRotate(/* degrees= */ 45);
int inputWidth = 200; int inputWidth = 200;
int inputHeight = 150; int inputHeight = 150;
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
new ScaleToFitFrameProcessor(getApplicationContext(), rotate45Matrix, C.LENGTH_UNSET); new ScaleToFitFrameProcessor.Builder(getApplicationContext())
.setRotationDegrees(45)
.build();
long expectedOutputWidthHeight = 247; long expectedOutputWidthHeight = 247;
Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight);
@ -144,12 +156,13 @@ public final class ScaleToFitFrameProcessorTest {
@Test @Test
public void configureOutputDimensions_setResolution_producesExpectedOutput() { public void configureOutputDimensions_setResolution_producesExpectedOutput() {
Matrix identityMatrix = new Matrix();
int inputWidth = 200; int inputWidth = 200;
int inputHeight = 150; int inputHeight = 150;
int requestedHeight = 300; int requestedHeight = 300;
ScaleToFitFrameProcessor scaleToFitFrameProcessor = ScaleToFitFrameProcessor scaleToFitFrameProcessor =
new ScaleToFitFrameProcessor(getApplicationContext(), identityMatrix, requestedHeight); new ScaleToFitFrameProcessor.Builder(getApplicationContext())
.setResolution(requestedHeight)
.build();
Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight);

View File

@ -18,7 +18,6 @@ package androidx.media3.transformer;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.graphics.Matrix;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test; import org.junit.Test;
@ -35,15 +34,12 @@ public class TransformationRequestTest {
} }
private static TransformationRequest createTestTransformationRequest() { private static TransformationRequest createTestTransformationRequest() {
Matrix transformationMatrix = new Matrix();
transformationMatrix.preRotate(36);
transformationMatrix.postTranslate((float) 0.5, (float) -0.2);
return new TransformationRequest.Builder() return new TransformationRequest.Builder()
.setFlattenForSlowMotion(true) .setFlattenForSlowMotion(true)
.setAudioMimeType(MimeTypes.AUDIO_AAC) .setAudioMimeType(MimeTypes.AUDIO_AAC)
.setVideoMimeType(MimeTypes.VIDEO_H264) .setVideoMimeType(MimeTypes.VIDEO_H264)
.setTransformationMatrix(transformationMatrix) .setRotationDegrees(45)
.setScale(/* scaleX= */ 1f, /* scaleY= */ 2f)
.build(); .build();
} }
} }