Add matrix provider for AdvancedFrameProcessor and examples in demo.

The matrix provider allows the transformation matrix to be updated
for each frame based on the timestamp.

The following example effects using this were added to the demo:
* a zoom-in transition for the start of the video,
* cropping a rotating rectangular frame portion,
* rotating the frame around the y-axis in 3D.

PiperOrigin-RevId: 439791592
This commit is contained in:
hschlueter 2022-04-06 11:28:55 +01:00 committed by Ian Baker
parent f75710be93
commit c235e4f447
6 changed files with 241 additions and 35 deletions

View File

@ -0,0 +1,96 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.demo.transformer;
import android.content.Context;
import android.graphics.Matrix;
import androidx.media3.common.C;
import androidx.media3.common.util.Util;
import androidx.media3.transformer.AdvancedFrameProcessor;
import androidx.media3.transformer.GlFrameProcessor;
/**
* Factory for {@link GlFrameProcessor GlFrameProcessors} that create video effects by applying
* transformation matrices to the individual video frames using {@link AdvancedFrameProcessor}.
*/
/* package */ final class AdvancedFrameProcessorFactory {
/**
* Returns a {@link GlFrameProcessor} that rescales the frames over the first {@value
* #ZOOM_DURATION_SECONDS} seconds, such that the rectangle filled with the input frame increases
* linearly in size from a single point to filling the full output frame.
*/
public static GlFrameProcessor createZoomInTransitionFrameProcessor(Context context) {
return new AdvancedFrameProcessor(
context,
/* matrixProvider= */ AdvancedFrameProcessorFactory::calculateZoomInTransitionMatrix);
}
/**
* Returns a {@link GlFrameProcessor} that crops frames to a rectangle that moves on an ellipse.
*/
public static GlFrameProcessor createDizzyCropFrameProcessor(Context context) {
return new AdvancedFrameProcessor(
context, /* matrixProvider= */ AdvancedFrameProcessorFactory::calculateDizzyCropMatrix);
}
/**
* Returns a {@link GlFrameProcessor} that rotates a frame in 3D around the y-axis and applies
* perspective projection to 2D.
*/
public static GlFrameProcessor createSpin3dFrameProcessor(Context context) {
return new AdvancedFrameProcessor(
context, /* matrixProvider= */ AdvancedFrameProcessorFactory::calculate3dSpinMatrix);
}
private static final float ZOOM_DURATION_SECONDS = 2f;
private static final float DIZZY_CROP_ROTATION_PERIOD_US = 1_500_000f;
private static Matrix calculateZoomInTransitionMatrix(long presentationTimeUs) {
Matrix transformationMatrix = new Matrix();
float scale = Math.min(1, presentationTimeUs / (C.MICROS_PER_SECOND * ZOOM_DURATION_SECONDS));
transformationMatrix.postScale(/* sx= */ scale, /* sy= */ scale);
return transformationMatrix;
}
private static android.graphics.Matrix calculateDizzyCropMatrix(long presentationTimeUs) {
double theta = presentationTimeUs * 2 * Math.PI / DIZZY_CROP_ROTATION_PERIOD_US;
float centerX = 0.5f * (float) Math.cos(theta);
float centerY = 0.5f * (float) Math.sin(theta);
android.graphics.Matrix transformationMatrix = new android.graphics.Matrix();
transformationMatrix.postTranslate(/* dx= */ centerX, /* dy= */ centerY);
transformationMatrix.postScale(/* sx= */ 2f, /* sy= */ 2f);
return transformationMatrix;
}
private static float[] calculate3dSpinMatrix(long presentationTimeUs) {
float[] transformationMatrix = new float[16];
android.opengl.Matrix.frustumM(
transformationMatrix,
/* offset= */ 0,
/* left= */ -1f,
/* right= */ 1f,
/* bottom= */ -1f,
/* top= */ 1f,
/* near= */ 3f,
/* far= */ 5f);
android.opengl.Matrix.translateM(
transformationMatrix, /* mOffset= */ 0, /* x= */ 0f, /* y= */ 0f, /* z= */ -4f);
float theta = Util.usToMs(presentationTimeUs) / 10f;
android.opengl.Matrix.rotateM(
transformationMatrix, /* mOffset= */ 0, theta, /* x= */ 0f, /* y= */ 1f, /* z= */ 0f);
return transformationMatrix;
}
}

View File

@ -56,6 +56,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
public static final String ENABLE_FALLBACK = "enable_fallback"; public static final String ENABLE_FALLBACK = "enable_fallback";
public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping"; public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping";
public static final String ENABLE_HDR_EDITING = "enable_hdr_editing"; public static final String ENABLE_HDR_EDITING = "enable_hdr_editing";
public static final String FRAME_PROCESSOR_SELECTION = "frame_processor_selection";
private static final String[] INPUT_URIS = { private static final String[] INPUT_URIS = {
"https://html5demos.com/assets/dizzy.mp4", "https://html5demos.com/assets/dizzy.mp4",
"https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4", "https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4",
@ -80,10 +81,11 @@ public final class ConfigurationActivity extends AppCompatActivity {
"SEF slow motion with 240 fps", "SEF slow motion with 240 fps",
"MP4 with HDR (HDR10) H265 video (encoding may fail)", "MP4 with HDR (HDR10) H265 video (encoding may fail)",
}; };
private static final String[] FRAME_PROCESSORS = {"Dizzy crop", "3D spin", "Zoom in start"};
private static final String SAME_AS_INPUT_OPTION = "same as input"; private static final String SAME_AS_INPUT_OPTION = "same as input";
private @MonotonicNonNull Button chooseFileButton; private @MonotonicNonNull Button selectFileButton;
private @MonotonicNonNull TextView chosenFileTextView; private @MonotonicNonNull TextView selectedFileTextView;
private @MonotonicNonNull CheckBox removeAudioCheckbox; private @MonotonicNonNull CheckBox removeAudioCheckbox;
private @MonotonicNonNull CheckBox removeVideoCheckbox; private @MonotonicNonNull CheckBox removeVideoCheckbox;
private @MonotonicNonNull CheckBox flattenForSlowMotionCheckbox; private @MonotonicNonNull CheckBox flattenForSlowMotionCheckbox;
@ -95,6 +97,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
private @MonotonicNonNull CheckBox enableFallbackCheckBox; private @MonotonicNonNull CheckBox enableFallbackCheckBox;
private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox; private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox;
private @MonotonicNonNull CheckBox enableHdrEditingCheckBox; private @MonotonicNonNull CheckBox enableHdrEditingCheckBox;
private @MonotonicNonNull Button selectFrameProcessorsButton;
private boolean @MonotonicNonNull [] selectedFrameProcessors;
private int inputUriPosition; private int inputUriPosition;
@Override @Override
@ -104,11 +108,11 @@ public final class ConfigurationActivity extends AppCompatActivity {
findViewById(R.id.transform_button).setOnClickListener(this::startTransformation); findViewById(R.id.transform_button).setOnClickListener(this::startTransformation);
chooseFileButton = findViewById(R.id.choose_file_button); selectFileButton = findViewById(R.id.select_file_button);
chooseFileButton.setOnClickListener(this::chooseFile); selectFileButton.setOnClickListener(this::selectFile);
chosenFileTextView = findViewById(R.id.chosen_file_text_view); selectedFileTextView = findViewById(R.id.selected_file_text_view);
chosenFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]);
removeAudioCheckbox = findViewById(R.id.remove_audio_checkbox); removeAudioCheckbox = findViewById(R.id.remove_audio_checkbox);
removeAudioCheckbox.setOnClickListener(this::onRemoveAudio); removeAudioCheckbox.setOnClickListener(this::onRemoveAudio);
@ -164,6 +168,10 @@ public final class ConfigurationActivity extends AppCompatActivity {
enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported()); enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported());
findViewById(R.id.request_sdr_tone_mapping).setEnabled(isRequestSdrToneMappingSupported()); findViewById(R.id.request_sdr_tone_mapping).setEnabled(isRequestSdrToneMappingSupported());
enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox); enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox);
selectedFrameProcessors = new boolean[FRAME_PROCESSORS.length];
selectFrameProcessorsButton = findViewById(R.id.select_frameprocessors_button);
selectFrameProcessorsButton.setOnClickListener(this::selectFrameProcessors);
} }
@Override @Override
@ -171,8 +179,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
super.onResume(); super.onResume();
@Nullable Uri intentUri = getIntent().getData(); @Nullable Uri intentUri = getIntent().getData();
if (intentUri != null) { if (intentUri != null) {
checkNotNull(chooseFileButton).setEnabled(false); checkNotNull(selectFileButton).setEnabled(false);
checkNotNull(chosenFileTextView).setText(intentUri.toString()); checkNotNull(selectedFileTextView).setText(intentUri.toString());
} }
} }
@ -193,7 +201,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
"rotateSpinner", "rotateSpinner",
"enableFallbackCheckBox", "enableFallbackCheckBox",
"enableRequestSdrToneMappingCheckBox", "enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox" "enableHdrEditingCheckBox",
"selectedFrameProcessors"
}) })
private void startTransformation(View view) { private void startTransformation(View view) {
Intent transformerIntent = new Intent(this, TransformerActivity.class); Intent transformerIntent = new Intent(this, TransformerActivity.class);
@ -228,6 +237,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
bundle.putBoolean( bundle.putBoolean(
ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked()); ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked());
bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked()); bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked());
bundle.putBooleanArray(FRAME_PROCESSOR_SELECTION, selectedFrameProcessors);
transformerIntent.putExtras(bundle); transformerIntent.putExtras(bundle);
@Nullable Uri intentUri = getIntent().getData(); @Nullable Uri intentUri = getIntent().getData();
@ -237,19 +247,34 @@ public final class ConfigurationActivity extends AppCompatActivity {
startActivity(transformerIntent); startActivity(transformerIntent);
} }
private void chooseFile(View view) { private void selectFile(View view) {
new AlertDialog.Builder(/* context= */ this) new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.choose_file_title) .setTitle(R.string.select_file_title)
.setSingleChoiceItems(URI_DESCRIPTIONS, inputUriPosition, this::selectFileInDialog) .setSingleChoiceItems(URI_DESCRIPTIONS, inputUriPosition, this::selectFileInDialog)
.setPositiveButton(android.R.string.ok, /* listener= */ null) .setPositiveButton(android.R.string.ok, /* listener= */ null)
.create() .create()
.show(); .show();
} }
@RequiresNonNull("chosenFileTextView") private void selectFrameProcessors(View view) {
new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.select_frameprocessors)
.setMultiChoiceItems(
FRAME_PROCESSORS, checkNotNull(selectedFrameProcessors), this::selectFrameProcessor)
.setPositiveButton(android.R.string.ok, /* listener= */ null)
.create()
.show();
}
@RequiresNonNull("selectedFileTextView")
private void selectFileInDialog(DialogInterface dialog, int which) { private void selectFileInDialog(DialogInterface dialog, int which) {
inputUriPosition = which; inputUriPosition = which;
chosenFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]);
}
@RequiresNonNull("selectedFrameProcessors")
private void selectFrameProcessor(DialogInterface dialog, int which, boolean isChecked) {
selectedFrameProcessors[which] = isChecked;
} }
@RequiresNonNull({ @RequiresNonNull({
@ -260,7 +285,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableRequestSdrToneMappingCheckBox", "enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox" "enableHdrEditingCheckBox",
"selectFrameProcessorsButton"
}) })
private void onRemoveAudio(View view) { private void onRemoveAudio(View view) {
if (((CheckBox) view).isChecked()) { if (((CheckBox) view).isChecked()) {
@ -279,7 +305,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableRequestSdrToneMappingCheckBox", "enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox" "enableHdrEditingCheckBox",
"selectFrameProcessorsButton"
}) })
private void onRemoveVideo(View view) { private void onRemoveVideo(View view) {
if (((CheckBox) view).isChecked()) { if (((CheckBox) view).isChecked()) {
@ -297,7 +324,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableRequestSdrToneMappingCheckBox", "enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox" "enableHdrEditingCheckBox",
"selectFrameProcessorsButton"
}) })
private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) { private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) {
audioMimeSpinner.setEnabled(isAudioEnabled); audioMimeSpinner.setEnabled(isAudioEnabled);
@ -308,6 +336,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
enableRequestSdrToneMappingCheckBox.setEnabled( enableRequestSdrToneMappingCheckBox.setEnabled(
isRequestSdrToneMappingSupported() && isVideoEnabled); isRequestSdrToneMappingSupported() && isVideoEnabled);
enableHdrEditingCheckBox.setEnabled(isVideoEnabled); enableHdrEditingCheckBox.setEnabled(isVideoEnabled);
selectFrameProcessorsButton.setEnabled(isVideoEnabled);
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);

View File

@ -40,6 +40,7 @@ import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.util.DebugTextViewHelper; import androidx.media3.exoplayer.util.DebugTextViewHelper;
import androidx.media3.transformer.DefaultEncoderFactory; import androidx.media3.transformer.DefaultEncoderFactory;
import androidx.media3.transformer.EncoderSelector; import androidx.media3.transformer.EncoderSelector;
import androidx.media3.transformer.GlFrameProcessor;
import androidx.media3.transformer.ProgressHolder; import androidx.media3.transformer.ProgressHolder;
import androidx.media3.transformer.TransformationException; import androidx.media3.transformer.TransformationException;
import androidx.media3.transformer.TransformationRequest; import androidx.media3.transformer.TransformationRequest;
@ -50,6 +51,7 @@ import androidx.media3.ui.PlayerView;
import com.google.android.material.progressindicator.LinearProgressIndicator; import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.common.base.Stopwatch; import com.google.common.base.Stopwatch;
import com.google.common.base.Ticker; import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@ -237,6 +239,27 @@ public final class TransformerActivity extends AppCompatActivity {
new DefaultEncoderFactory( new DefaultEncoderFactory(
EncoderSelector.DEFAULT, EncoderSelector.DEFAULT,
/* enableFallback= */ bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK))); /* enableFallback= */ bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK)));
ImmutableList.Builder<GlFrameProcessor> frameProcessors = new ImmutableList.Builder<>();
@Nullable
boolean[] selectedFrameProcessors =
bundle.getBooleanArray(ConfigurationActivity.FRAME_PROCESSOR_SELECTION);
if (selectedFrameProcessors != null) {
if (selectedFrameProcessors[0]) {
frameProcessors.add(
AdvancedFrameProcessorFactory.createDizzyCropFrameProcessor(/* context= */ this));
}
if (selectedFrameProcessors[1]) {
frameProcessors.add(
AdvancedFrameProcessorFactory.createSpin3dFrameProcessor(/* context= */ this));
}
if (selectedFrameProcessors[2]) {
frameProcessors.add(
AdvancedFrameProcessorFactory.createZoomInTransitionFrameProcessor(
/* context= */ this));
}
transformerBuilder.setFrameProcessors(frameProcessors.build());
}
} }
return transformerBuilder return transformerBuilder
.addListener( .addListener(

View File

@ -34,18 +34,18 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<Button <Button
android:id="@+id/choose_file_button" android:id="@+id/select_file_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
android:layout_marginStart="32dp" android:layout_marginStart="32dp"
android:layout_marginEnd="32dp" android:layout_marginEnd="32dp"
android:text="@string/choose_file_title" android:text="@string/select_file_title"
app:layout_constraintTop_toBottomOf="@+id/configuration_text_view" app:layout_constraintTop_toBottomOf="@+id/configuration_text_view"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<TextView <TextView
android:id="@+id/chosen_file_text_view" android:id="@+id/selected_file_text_view"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
@ -57,14 +57,14 @@
android:gravity="center" android:gravity="center"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/choose_file_button" /> app:layout_constraintTop_toBottomOf="@+id/select_file_button" />
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/chosen_file_text_view" app:layout_constraintTop_toBottomOf="@+id/selected_file_text_view"
app:layout_constraintBottom_toTopOf="@+id/transform_button"> app:layout_constraintBottom_toTopOf="@+id/select_frameprocessors_button">
<TableLayout <TableLayout
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -191,6 +191,17 @@
</TableRow> </TableRow>
</TableLayout> </TableLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
<Button
android:id="@+id/select_frameprocessors_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:text="@string/select_frameprocessors"
app:layout_constraintBottom_toTopOf="@+id/transform_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button <Button
android:id="@+id/transform_button" android:id="@+id/transform_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -17,7 +17,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name" translatable="false">Transformer Demo</string> <string name="app_name" translatable="false">Transformer Demo</string>
<string name="configuration" translatable="false">Configuration</string> <string name="configuration" translatable="false">Configuration</string>
<string name="choose_file_title" translatable="false">Choose file</string> <string name="select_file_title" translatable="false">Choose file</string>
<string name="remove_audio" translatable="false">Remove audio</string> <string name="remove_audio" translatable="false">Remove audio</string>
<string name="remove_video" translatable="false">Remove video</string> <string name="remove_video" translatable="false">Remove video</string>
<string name="flatten_for_slow_motion" translatable="false">Flatten for slow motion</string> <string name="flatten_for_slow_motion" translatable="false">Flatten for slow motion</string>
@ -29,6 +29,7 @@
<string name="enable_fallback" translatable="false">Enable fallback</string> <string name="enable_fallback" translatable="false">Enable fallback</string>
<string name="request_sdr_tone_mapping" translatable="false">Request SDR tone-mapping (API 31+)</string> <string name="request_sdr_tone_mapping" translatable="false">Request SDR tone-mapping (API 31+)</string>
<string name="hdr_editing" translatable="false">[Experimental] HDR editing</string> <string name="hdr_editing" translatable="false">[Experimental] HDR editing</string>
<string name="select_frameprocessors" translatable="false">Add effects</string>
<string name="transform" translatable="false">Transform</string> <string name="transform" translatable="false">Transform</string>
<string name="debug_preview" translatable="false">Debug preview:</string> <string name="debug_preview" translatable="false">Debug preview:</string>
<string name="debug_preview_not_available" translatable="false">No debug preview available.</string> <string name="debug_preview_not_available" translatable="false">No debug preview available.</string>

View File

@ -19,7 +19,6 @@ import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context; import android.content.Context;
import android.graphics.Matrix;
import android.opengl.GLES20; import android.opengl.GLES20;
import android.util.Size; import android.util.Size;
import androidx.media3.common.util.GlProgram; import androidx.media3.common.util.GlProgram;
@ -36,23 +35,47 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* Width and height are not modified. The background color will default to black. * Width and height are not modified. The background color will default to black.
*/ */
@UnstableApi @UnstableApi
@SuppressWarnings("FunctionalInterfaceClash") // b/228192298
public final class AdvancedFrameProcessor implements GlFrameProcessor { public final class AdvancedFrameProcessor implements GlFrameProcessor {
static { static {
GlUtil.glAssertionsEnabled = true; GlUtil.glAssertionsEnabled = true;
} }
/** Updates the transformation {@link android.opengl.Matrix} for each frame. */
public interface GlMatrixProvider {
/**
* Updates the transformation {@link android.opengl.Matrix} to apply to the frame with the given
* timestamp in place.
*/
float[] getGlMatrixArray(long presentationTimeUs);
}
/** Provides a {@link android.graphics.Matrix} for each frame. */
public interface MatrixProvider extends GlMatrixProvider {
/**
* Returns the transformation {@link android.graphics.Matrix} to apply to the frame with the
* given timestamp.
*/
android.graphics.Matrix getMatrix(long presentationTimeUs);
@Override
default float[] getGlMatrixArray(long presentationTimeUs) {
return AdvancedFrameProcessor.getGlMatrixArray(getMatrix(presentationTimeUs));
}
}
private static final String VERTEX_SHADER_TRANSFORMATION_PATH = private static final String VERTEX_SHADER_TRANSFORMATION_PATH =
"shaders/vertex_shader_transformation_es2.glsl"; "shaders/vertex_shader_transformation_es2.glsl";
private static final String FRAGMENT_SHADER_PATH = "shaders/fragment_shader_copy_es2.glsl"; private static final String FRAGMENT_SHADER_PATH = "shaders/fragment_shader_copy_es2.glsl";
/** /**
* Returns a 4x4, column-major {@link android.opengl.Matrix} float array, from an input {@link * Returns a 4x4, column-major {@link android.opengl.Matrix} float array, from an input {@link
* Matrix}. * android.graphics.Matrix}.
* *
* <p>This is useful for converting to the 4x4 column-major format commonly used in OpenGL. * <p>This is useful for converting to the 4x4 column-major format commonly used in OpenGL.
*/ */
private static float[] getGlMatrixArray(Matrix matrix) { private static float[] getGlMatrixArray(android.graphics.Matrix matrix) {
float[] matrix3x3Array = new float[9]; float[] matrix3x3Array = new float[9];
matrix.getValues(matrix3x3Array); matrix.getValues(matrix3x3Array);
float[] matrix4x4Array = getMatrix4x4Array(matrix3x3Array); float[] matrix4x4Array = getMatrix4x4Array(matrix3x3Array);
@ -89,7 +112,7 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor {
} }
private final Context context; private final Context context;
private final float[] transformationMatrix; private final GlMatrixProvider matrixProvider;
private @MonotonicNonNull Size size; private @MonotonicNonNull Size size;
private @MonotonicNonNull GlProgram glProgram; private @MonotonicNonNull GlProgram glProgram;
@ -98,14 +121,26 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor {
* Creates a new instance. * Creates a new instance.
* *
* @param context The {@link Context}. * @param context The {@link Context}.
* @param transformationMatrix The transformation {@link Matrix} to apply to each frame. * @param transformationMatrix The transformation {@link android.graphics.Matrix} to apply to each
* Operations are done on normalized device coordinates (-1 to 1 on x and y), and no automatic * frame. Operations are done on normalized device coordinates (-1 to 1 on x and y), and no
* adjustments are applied on the transformation matrix. * automatic adjustments are applied on the transformation matrix.
*/ */
public AdvancedFrameProcessor(Context context, Matrix transformationMatrix) { public AdvancedFrameProcessor(Context context, android.graphics.Matrix transformationMatrix) {
this(context, getGlMatrixArray(transformationMatrix)); this(context, getGlMatrixArray(transformationMatrix));
} }
/**
* Creates a new instance.
*
* @param context The {@link Context}.
* @param matrixProvider A {@link MatrixProvider} that provides the transformation matrix to apply
* to each frame.
*/
public AdvancedFrameProcessor(Context context, MatrixProvider matrixProvider) {
this.context = context;
this.matrixProvider = matrixProvider;
}
/** /**
* Creates a new instance. * Creates a new instance.
* *
@ -115,10 +150,21 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor {
* no automatic adjustments are applied on the transformation matrix. * no automatic adjustments are applied on the transformation matrix.
*/ */
public AdvancedFrameProcessor(Context context, float[] transformationMatrix) { public AdvancedFrameProcessor(Context context, float[] transformationMatrix) {
this(context, /* matrixProvider= */ (long presentationTimeUs) -> transformationMatrix.clone());
checkArgument( checkArgument(
transformationMatrix.length == 16, "A 4x4 transformation matrix must have 16 elements."); transformationMatrix.length == 16, "A 4x4 transformation matrix must have 16 elements.");
}
/**
* Creates a new instance.
*
* @param context The {@link Context}.
* @param matrixProvider A {@link GlMatrixProvider} that updates the transformation matrix for
* each frame.
*/
public AdvancedFrameProcessor(Context context, GlMatrixProvider matrixProvider) {
this.context = context; this.context = context;
this.transformationMatrix = transformationMatrix.clone(); this.matrixProvider = matrixProvider;
} }
@Override @Override
@ -133,7 +179,6 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor {
"aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); "aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
glProgram.setBufferAttribute( glProgram.setBufferAttribute(
"aTexSamplingCoord", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); "aTexSamplingCoord", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
glProgram.setFloatsUniform("uTransformationMatrix", transformationMatrix);
} }
@Override @Override
@ -143,8 +188,9 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor {
@Override @Override
public void updateProgramAndDraw(long presentationTimeUs) { public void updateProgramAndDraw(long presentationTimeUs) {
checkStateNotNull(glProgram); checkStateNotNull(glProgram).use();
glProgram.use(); glProgram.setFloatsUniform(
"uTransformationMatrix", matrixProvider.getGlMatrixArray(presentationTimeUs));
glProgram.bindAttributesAndUniforms(); glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad. // The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);