From ce4a0288296a0f9d2c4e7c16040da1acc29e33bc Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 28 Jan 2022 13:55:48 +0000 Subject: [PATCH] Publish the transformer demo app PiperOrigin-RevId: 424850283 --- constants.gradle | 1 + demos/transformer/README.md | 9 + demos/transformer/build.gradle | 60 +++ .../transformer/src/main/AndroidManifest.xml | 61 +++ .../transformer/ConfigurationActivity.java | 302 ++++++++++++++ .../demo/transformer/TransformerActivity.java | 372 ++++++++++++++++++ .../media3/demo/transformer/package-info.java | 19 + .../res/layout/configuration_activity.xml | 179 +++++++++ .../src/main/res/layout/spinner_item.xml | 26 ++ .../main/res/layout/transformer_activity.xml | 107 +++++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3394 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2184 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4886 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7492 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10801 bytes .../src/main/res/values/strings.xml | 37 ++ settings.gradle | 2 + 17 files changed, 1175 insertions(+) create mode 100644 demos/transformer/README.md create mode 100644 demos/transformer/build.gradle create mode 100644 demos/transformer/src/main/AndroidManifest.xml create mode 100644 demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java create mode 100644 demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java create mode 100644 demos/transformer/src/main/java/androidx/media3/demo/transformer/package-info.java create mode 100644 demos/transformer/src/main/res/layout/configuration_activity.xml create mode 100644 demos/transformer/src/main/res/layout/spinner_item.xml create mode 100644 demos/transformer/src/main/res/layout/transformer_activity.xml create mode 100644 demos/transformer/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 demos/transformer/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 demos/transformer/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 demos/transformer/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 demos/transformer/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 demos/transformer/src/main/res/values/strings.xml diff --git a/constants.gradle b/constants.gradle index 50ddb74592..7805a8dfb9 100644 --- a/constants.gradle +++ b/constants.gradle @@ -37,6 +37,7 @@ project.ext { androidxAnnotationExperimentalVersion = '1.2.0' androidxAppCompatVersion = '1.3.1' androidxCollectionVersion = '1.1.0' + androidxConstraintLayoutVersion = '2.0.4' androidxCoreVersion = '1.7.0' androidxFuturesVersion = '1.1.0' androidxMediaVersion = '1.4.3' diff --git a/demos/transformer/README.md b/demos/transformer/README.md new file mode 100644 index 0000000000..fb2657001e --- /dev/null +++ b/demos/transformer/README.md @@ -0,0 +1,9 @@ +# Transformer demo + +This app demonstrates how to use the [Transformer][] API to modify videos, for +example by removing audio or video. + +See the [demos README](../README.md) for instructions on how to build and run +this demo. + +[Transformer]: https://exoplayer.dev/transforming-media.html diff --git a/demos/transformer/build.gradle b/demos/transformer/build.gradle new file mode 100644 index 0000000000..14825588ba --- /dev/null +++ b/demos/transformer/build.gradle @@ -0,0 +1,60 @@ +/* + * Copyright 2021 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. + */ +apply from: '../../constants.gradle' +apply plugin: 'com.android.application' + +android { + compileSdkVersion project.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + versionName project.ext.releaseVersion + versionCode project.ext.releaseVersionCode + minSdkVersion 21 + targetSdkVersion project.ext.appTargetSdkVersion + multiDexEnabled true + } + + buildTypes { + release { + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt') + signingConfig signingConfigs.debug + } + } + + lintOptions { + // This demo app isn't indexed and doesn't have translations. + disable 'GoogleAppIndexingWarning','MissingTranslation' + } +} + +dependencies { + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion + implementation 'androidx.constraintlayout:constraintlayout:' + androidxConstraintLayoutVersion + implementation 'androidx.multidex:multidex:' + androidxMultidexVersion + implementation 'com.google.android.material:material:' + androidxMaterialVersion + implementation project(modulePrefix + 'lib-exoplayer') + implementation project(modulePrefix + 'lib-transformer') + implementation project(modulePrefix + 'lib-ui') +} diff --git a/demos/transformer/src/main/AndroidManifest.xml b/demos/transformer/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..b14bc3622b --- /dev/null +++ b/demos/transformer/src/main/AndroidManifest.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java new file mode 100644 index 0000000000..de0369c346 --- /dev/null +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java @@ -0,0 +1,302 @@ +/* + * Copyright 2021 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 static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; + +import android.app.Activity; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.Spinner; +import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.media3.common.MimeTypes; +import java.util.Arrays; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +/** + * An {@link Activity} that sets the configuration to use for transforming and playing media, using + * {@link TransformerActivity}. + */ +public final class ConfigurationActivity extends AppCompatActivity { + public static final String SHOULD_REMOVE_AUDIO = "should_remove_audio"; + public static final String SHOULD_REMOVE_VIDEO = "should_remove_video"; + public static final String SHOULD_FLATTEN_FOR_SLOW_MOTION = "should_flatten_for_slow_motion"; + public static final String AUDIO_MIME_TYPE = "audio_mime_type"; + public static final String VIDEO_MIME_TYPE = "video_mime_type"; + 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_Y = "scale_y"; + public static final String ROTATE_DEGREES = "rotate_degrees"; + private static final String[] INPUT_URIS = { + "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/BigBuckBunny_320x180.mp4", + "https://html5demos.com/assets/dizzy.webm", + }; + private static final String[] URI_DESCRIPTIONS = { // same order as INPUT_URIS + "MP4 with H264 video and AAC audio", + "MP4 with H265 video and AAC audio", + "Long MP4 with H264 video and AAC audio", + "WebM with VP8 video and Vorbis audio", + }; + private static final String SAME_AS_INPUT_OPTION = "same as input"; + + private @MonotonicNonNull Button chooseFileButton; + private @MonotonicNonNull CheckBox removeAudioCheckbox; + private @MonotonicNonNull CheckBox removeVideoCheckbox; + private @MonotonicNonNull CheckBox flattenForSlowMotionCheckbox; + private @MonotonicNonNull Spinner audioMimeSpinner; + private @MonotonicNonNull Spinner videoMimeSpinner; + private @MonotonicNonNull Spinner resolutionHeightSpinner; + private @MonotonicNonNull Spinner translateSpinner; + private @MonotonicNonNull Spinner scaleSpinner; + private @MonotonicNonNull Spinner rotateSpinner; + private @MonotonicNonNull TextView chosenFileTextView; + private int inputUriPosition; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.configuration_activity); + + findViewById(R.id.transform_button).setOnClickListener(this::startTransformation); + + chooseFileButton = findViewById(R.id.choose_file_button); + chooseFileButton.setOnClickListener(this::chooseFile); + + chosenFileTextView = findViewById(R.id.chosen_file_text_view); + chosenFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); + + removeAudioCheckbox = findViewById(R.id.remove_audio_checkbox); + removeAudioCheckbox.setOnClickListener(this::onRemoveAudio); + + removeVideoCheckbox = findViewById(R.id.remove_video_checkbox); + removeVideoCheckbox.setOnClickListener(this::onRemoveVideo); + + flattenForSlowMotionCheckbox = findViewById(R.id.flatten_for_slow_motion_checkbox); + + ArrayAdapter audioMimeAdapter = + new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); + audioMimeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + audioMimeSpinner = findViewById(R.id.audio_mime_spinner); + audioMimeSpinner.setAdapter(audioMimeAdapter); + audioMimeAdapter.addAll( + SAME_AS_INPUT_OPTION, MimeTypes.AUDIO_AAC, MimeTypes.AUDIO_AMR_NB, MimeTypes.AUDIO_AMR_WB); + + ArrayAdapter videoMimeAdapter = + new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); + videoMimeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + videoMimeSpinner = findViewById(R.id.video_mime_spinner); + videoMimeSpinner.setAdapter(videoMimeAdapter); + videoMimeAdapter.addAll( + SAME_AS_INPUT_OPTION, + MimeTypes.VIDEO_H263, + MimeTypes.VIDEO_H264, + MimeTypes.VIDEO_H265, + MimeTypes.VIDEO_MP4V); + + ArrayAdapter resolutionHeightAdapter = + new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); + resolutionHeightAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + resolutionHeightSpinner = findViewById(R.id.resolution_height_spinner); + resolutionHeightSpinner.setAdapter(resolutionHeightAdapter); + resolutionHeightAdapter.addAll( + SAME_AS_INPUT_OPTION, "144", "240", "360", "480", "720", "1080", "1440", "2160"); + + ArrayAdapter 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 scaleAdapter = + new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); + scaleAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + scaleSpinner = findViewById(R.id.scale_spinner); + scaleSpinner.setAdapter(scaleAdapter); + scaleAdapter.addAll(SAME_AS_INPUT_OPTION, "-1, -1", "-1, 1", "1, 1", ".5, 1", ".5, .5", "2, 2"); + + ArrayAdapter rotateAdapter = + new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); + rotateAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + rotateSpinner = findViewById(R.id.rotate_spinner); + rotateSpinner.setAdapter(rotateAdapter); + rotateAdapter.addAll(SAME_AS_INPUT_OPTION, "0", "10", "45", "90", "180"); + } + + @Override + protected void onResume() { + super.onResume(); + @Nullable Uri intentUri = getIntent().getData(); + if (intentUri != null) { + checkNotNull(chooseFileButton).setEnabled(false); + checkNotNull(chosenFileTextView).setText(intentUri.toString()); + } + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } + + @RequiresNonNull({ + "removeAudioCheckbox", + "removeVideoCheckbox", + "flattenForSlowMotionCheckbox", + "audioMimeSpinner", + "videoMimeSpinner", + "resolutionHeightSpinner", + "translateSpinner", + "scaleSpinner", + "rotateSpinner" + }) + private void startTransformation(View view) { + Intent transformerIntent = new Intent(this, TransformerActivity.class); + Bundle bundle = new Bundle(); + bundle.putBoolean(SHOULD_REMOVE_AUDIO, removeAudioCheckbox.isChecked()); + bundle.putBoolean(SHOULD_REMOVE_VIDEO, removeVideoCheckbox.isChecked()); + bundle.putBoolean(SHOULD_FLATTEN_FOR_SLOW_MOTION, flattenForSlowMotionCheckbox.isChecked()); + String selectedAudioMimeType = String.valueOf(audioMimeSpinner.getSelectedItem()); + if (!SAME_AS_INPUT_OPTION.equals(selectedAudioMimeType)) { + bundle.putString(AUDIO_MIME_TYPE, selectedAudioMimeType); + } + String selectedVideoMimeType = String.valueOf(videoMimeSpinner.getSelectedItem()); + if (!SAME_AS_INPUT_OPTION.equals(selectedVideoMimeType)) { + bundle.putString(VIDEO_MIME_TYPE, selectedVideoMimeType); + } + String selectedResolutionHeight = String.valueOf(resolutionHeightSpinner.getSelectedItem()); + if (!SAME_AS_INPUT_OPTION.equals(selectedResolutionHeight)) { + bundle.putInt(RESOLUTION_HEIGHT, Integer.valueOf(selectedResolutionHeight)); + } + String selectedTranslate = String.valueOf(translateSpinner.getSelectedItem()); + if (!SAME_AS_INPUT_OPTION.equals(selectedTranslate)) { + List translateXY = Arrays.asList(selectedTranslate.split(", ")); + checkState(translateXY.size() == 2); + bundle.putFloat(TRANSLATE_X, Float.valueOf(translateXY.get(0))); + bundle.putFloat(TRANSLATE_Y, Float.valueOf(translateXY.get(1))); + } + String selectedScale = String.valueOf(scaleSpinner.getSelectedItem()); + if (!SAME_AS_INPUT_OPTION.equals(selectedScale)) { + List scaleXY = Arrays.asList(selectedScale.split(", ")); + checkState(scaleXY.size() == 2); + bundle.putFloat(SCALE_X, Float.valueOf(scaleXY.get(0))); + bundle.putFloat(SCALE_Y, Float.valueOf(scaleXY.get(1))); + } + String selectedRotate = String.valueOf(rotateSpinner.getSelectedItem()); + if (!SAME_AS_INPUT_OPTION.equals(selectedRotate)) { + bundle.putFloat(ROTATE_DEGREES, Float.valueOf(selectedRotate)); + } + transformerIntent.putExtras(bundle); + + @Nullable Uri intentUri = getIntent().getData(); + transformerIntent.setData( + intentUri != null ? intentUri : Uri.parse(INPUT_URIS[inputUriPosition])); + + startActivity(transformerIntent); + } + + private void chooseFile(View view) { + new AlertDialog.Builder(/* context= */ this) + .setTitle(R.string.choose_file_title) + .setSingleChoiceItems(URI_DESCRIPTIONS, inputUriPosition, this::selectFileInDialog) + .setPositiveButton(android.R.string.ok, /* listener= */ null) + .create() + .show(); + } + + @RequiresNonNull("chosenFileTextView") + private void selectFileInDialog(DialogInterface dialog, int which) { + inputUriPosition = which; + chosenFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); + } + + @RequiresNonNull({ + "removeVideoCheckbox", + "audioMimeSpinner", + "videoMimeSpinner", + "resolutionHeightSpinner", + "translateSpinner", + "scaleSpinner", + "rotateSpinner" + }) + private void onRemoveAudio(View view) { + if (((CheckBox) view).isChecked()) { + removeVideoCheckbox.setChecked(false); + enableTrackSpecificOptions(/* isAudioEnabled= */ false, /* isVideoEnabled= */ true); + } else { + enableTrackSpecificOptions(/* isAudioEnabled= */ true, /* isVideoEnabled= */ true); + } + } + + @RequiresNonNull({ + "removeAudioCheckbox", + "audioMimeSpinner", + "videoMimeSpinner", + "resolutionHeightSpinner", + "translateSpinner", + "scaleSpinner", + "rotateSpinner" + }) + private void onRemoveVideo(View view) { + if (((CheckBox) view).isChecked()) { + removeAudioCheckbox.setChecked(false); + enableTrackSpecificOptions(/* isAudioEnabled= */ true, /* isVideoEnabled= */ false); + } else { + enableTrackSpecificOptions(/* isAudioEnabled= */ true, /* isVideoEnabled= */ true); + } + } + + @RequiresNonNull({ + "audioMimeSpinner", + "videoMimeSpinner", + "resolutionHeightSpinner", + "translateSpinner", + "scaleSpinner", + "rotateSpinner" + }) + private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) { + audioMimeSpinner.setEnabled(isAudioEnabled); + videoMimeSpinner.setEnabled(isVideoEnabled); + resolutionHeightSpinner.setEnabled(isVideoEnabled); + translateSpinner.setEnabled(isVideoEnabled); + scaleSpinner.setEnabled(isVideoEnabled); + rotateSpinner.setEnabled(isVideoEnabled); + + findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled); + findViewById(R.id.video_mime_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.rotate).setEnabled(isVideoEnabled); + } +} diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java new file mode 100644 index 0000000000..f82145ab84 --- /dev/null +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java @@ -0,0 +1,372 @@ +/* + * Copyright 2021 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 static android.Manifest.permission.READ_EXTERNAL_STORAGE; +import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Matrix; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.media3.common.C; +import androidx.media3.common.MediaItem; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.exoplayer.util.DebugTextViewHelper; +import androidx.media3.transformer.ProgressHolder; +import androidx.media3.transformer.TransformationException; +import androidx.media3.transformer.TransformationRequest; +import androidx.media3.transformer.Transformer; +import androidx.media3.ui.AspectRatioFrameLayout; +import androidx.media3.ui.PlayerView; +import com.google.android.material.progressindicator.LinearProgressIndicator; +import com.google.common.base.Stopwatch; +import com.google.common.base.Ticker; +import java.io.File; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +/** An {@link Activity} that transforms and plays media using {@link Transformer}. */ +public final class TransformerActivity extends AppCompatActivity { + private static final String TAG = "TransformerActivity"; + + private @MonotonicNonNull PlayerView playerView; + private @MonotonicNonNull TextView debugTextView; + private @MonotonicNonNull TextView informationTextView; + private @MonotonicNonNull ViewGroup progressViewGroup; + private @MonotonicNonNull LinearProgressIndicator progressIndicator; + private @MonotonicNonNull Stopwatch transformationStopwatch; + private @MonotonicNonNull AspectRatioFrameLayout debugFrame; + + @Nullable private DebugTextViewHelper debugTextViewHelper; + @Nullable private ExoPlayer player; + @Nullable private Transformer transformer; + @Nullable private File externalCacheFile; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.transformer_activity); + + playerView = findViewById(R.id.player_view); + debugTextView = findViewById(R.id.debug_text_view); + informationTextView = findViewById(R.id.information_text_view); + progressViewGroup = findViewById(R.id.progress_view_group); + progressIndicator = findViewById(R.id.progress_indicator); + debugFrame = findViewById(R.id.debug_aspect_ratio_frame_layout); + + transformationStopwatch = + Stopwatch.createUnstarted( + new Ticker() { + public long read() { + return android.os.SystemClock.elapsedRealtimeNanos(); + } + }); + } + + @Override + protected void onStart() { + super.onStart(); + + checkNotNull(progressIndicator); + checkNotNull(informationTextView); + checkNotNull(transformationStopwatch); + checkNotNull(playerView); + checkNotNull(debugTextView); + checkNotNull(progressViewGroup); + startTransformation(); + + playerView.onResume(); + } + + @Override + protected void onStop() { + super.onStop(); + + checkNotNull(transformationStopwatch).reset(); + + checkNotNull(transformer).cancel(); + transformer = null; + + checkNotNull(playerView).onPause(); + releasePlayer(); + + checkNotNull(externalCacheFile).delete(); + externalCacheFile = null; + } + + @RequiresNonNull({ + "playerView", + "debugTextView", + "informationTextView", + "progressIndicator", + "transformationStopwatch", + "progressViewGroup", + }) + private void startTransformation() { + requestTransformerPermissions(); + + Intent intent = getIntent(); + Uri uri = checkNotNull(intent.getData()); + try { + externalCacheFile = createExternalCacheFile("transformer-output.mp4"); + String filePath = externalCacheFile.getAbsolutePath(); + @Nullable Bundle bundle = intent.getExtras(); + Transformer transformer = createTransformer(bundle, filePath); + transformationStopwatch.start(); + transformer.startTransformation(MediaItem.fromUri(uri), filePath); + this.transformer = transformer; + } catch (IOException e) { + throw new IllegalStateException(e); + } + informationTextView.setText(R.string.transformation_started); + Handler mainHandler = new Handler(getMainLooper()); + ProgressHolder progressHolder = new ProgressHolder(); + mainHandler.post( + new Runnable() { + @Override + public void run() { + if (transformer != null + && transformer.getProgress(progressHolder) + != Transformer.PROGRESS_STATE_NO_TRANSFORMATION) { + progressIndicator.setProgress(progressHolder.progress); + informationTextView.setText( + getString( + R.string.transformation_timer, + transformationStopwatch.elapsed(TimeUnit.SECONDS))); + mainHandler.postDelayed(/* r= */ this, /* delayMillis= */ 500); + } + } + }); + } + + // Create a cache file, resetting it if it already exists. + private File createExternalCacheFile(String fileName) throws IOException { + File file = new File(getExternalCacheDir(), fileName); + if (file.exists() && !file.delete()) { + throw new IllegalStateException("Could not delete the previous transformer output file"); + } + if (!file.createNewFile()) { + throw new IllegalStateException("Could not create the transformer output file"); + } + return file; + } + + @RequiresNonNull({ + "playerView", + "debugTextView", + "informationTextView", + "transformationStopwatch", + "progressViewGroup", + }) + private Transformer createTransformer(@Nullable Bundle bundle, String filePath) { + Transformer.Builder transformerBuilder = new Transformer.Builder(/* context= */ this); + if (bundle != null) { + TransformationRequest.Builder requestBuilder = new TransformationRequest.Builder(); + requestBuilder.setFlattenForSlowMotion( + bundle.getBoolean(ConfigurationActivity.SHOULD_FLATTEN_FOR_SLOW_MOTION)); + @Nullable String audioMimeType = bundle.getString(ConfigurationActivity.AUDIO_MIME_TYPE); + if (audioMimeType != null) { + requestBuilder.setAudioMimeType(audioMimeType); + } + @Nullable String videoMimeType = bundle.getString(ConfigurationActivity.VIDEO_MIME_TYPE); + if (videoMimeType != null) { + requestBuilder.setVideoMimeType(videoMimeType); + } + int resolutionHeight = + bundle.getInt( + ConfigurationActivity.RESOLUTION_HEIGHT, /* defaultValue= */ C.LENGTH_UNSET); + if (resolutionHeight != C.LENGTH_UNSET) { + requestBuilder.setResolution(resolutionHeight); + } + Matrix transformationMatrix = getTransformationMatrix(bundle); + if (!transformationMatrix.isIdentity()) { + requestBuilder.setTransformationMatrix(transformationMatrix); + } + transformerBuilder + .setTransformationRequest(requestBuilder.build()) + .setRemoveAudio(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_AUDIO)) + .setRemoveVideo(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_VIDEO)); + } + return transformerBuilder + .addListener( + new Transformer.Listener() { + @Override + public void onTransformationCompleted(MediaItem mediaItem) { + TransformerActivity.this.onTransformationCompleted(filePath); + } + + @Override + public void onTransformationError( + MediaItem mediaItem, TransformationException exception) { + TransformerActivity.this.onTransformationError(exception); + } + }) + .setDebugViewProvider(new DemoDebugViewProvider()) + .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/213198690): Get resolution for aspect ratio and scale all translations' translateX + // by this aspect ratio. + 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({ + "informationTextView", + "progressViewGroup", + "transformationStopwatch", + }) + private void onTransformationError(TransformationException exception) { + transformationStopwatch.stop(); + informationTextView.setText(R.string.transformation_error); + progressViewGroup.setVisibility(View.GONE); + Toast.makeText( + TransformerActivity.this, "Transformation error: " + exception, Toast.LENGTH_LONG) + .show(); + Log.e(TAG, "Transformation error", exception); + } + + @RequiresNonNull({ + "playerView", + "debugTextView", + "informationTextView", + "progressViewGroup", + "transformationStopwatch", + }) + private void onTransformationCompleted(String filePath) { + transformationStopwatch.stop(); + informationTextView.setText( + getString( + R.string.transformation_completed, transformationStopwatch.elapsed(TimeUnit.SECONDS))); + progressViewGroup.setVisibility(View.GONE); + playMediaItem(MediaItem.fromUri("file://" + filePath)); + Log.d(TAG, "Output file path: file://" + filePath); + } + + @RequiresNonNull({"playerView", "debugTextView"}) + private void playMediaItem(MediaItem mediaItem) { + playerView.setPlayer(null); + releasePlayer(); + + ExoPlayer player = new ExoPlayer.Builder(/* context= */ this).build(); + playerView.setPlayer(player); + player.setMediaItem(mediaItem); + player.play(); + player.prepare(); + this.player = player; + debugTextViewHelper = new DebugTextViewHelper(player, debugTextView); + debugTextViewHelper.start(); + } + + private void releasePlayer() { + if (debugTextViewHelper != null) { + debugTextViewHelper.stop(); + debugTextViewHelper = null; + } + if (player != null) { + player.release(); + player = null; + } + } + + private void requestTransformerPermissions() { + if (Util.SDK_INT < 23) { + return; + } + if (checkSelfPermission(READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED + || checkSelfPermission(WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + new String[] {READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE}, /* requestCode= */ 0); + } + } + + private final class DemoDebugViewProvider implements Transformer.DebugViewProvider { + + @Nullable + @Override + public SurfaceView getDebugPreviewSurfaceView(int width, int height) { + // Update the UI on the main thread and wait for the output surface to be available. + CountDownLatch surfaceCreatedCountDownLatch = new CountDownLatch(1); + SurfaceView surfaceView = new SurfaceView(/* context= */ TransformerActivity.this); + runOnUiThread( + () -> { + AspectRatioFrameLayout debugFrame = checkNotNull(TransformerActivity.this.debugFrame); + debugFrame.addView(surfaceView); + debugFrame.setAspectRatio((float) width / height); + surfaceView + .getHolder() + .addCallback( + new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder surfaceHolder) { + surfaceCreatedCountDownLatch.countDown(); + } + + @Override + public void surfaceChanged( + SurfaceHolder surfaceHolder, int format, int width, int height) { + // Do nothing. + } + + @Override + public void surfaceDestroyed(SurfaceHolder surfaceHolder) { + // Do nothing. + } + }); + }); + try { + surfaceCreatedCountDownLatch.await(); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted waiting for debug surface."); + Thread.currentThread().interrupt(); + return null; + } + return surfaceView; + } + } +} diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/package-info.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/package-info.java new file mode 100644 index 0000000000..8a243704e2 --- /dev/null +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2021 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. + */ +@NonNullApi +package androidx.media3.demo.transformer; + +import androidx.media3.common.util.NonNullApi; diff --git a/demos/transformer/src/main/res/layout/configuration_activity.xml b/demos/transformer/src/main/res/layout/configuration_activity.xml new file mode 100644 index 0000000000..a9a9410a35 --- /dev/null +++ b/demos/transformer/src/main/res/layout/configuration_activity.xml @@ -0,0 +1,179 @@ + + + + + +