1 /* 2 * Copyright 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.exoplayer2.transformerdemo; 17 18 import static com.google.android.exoplayer2.util.Assertions.checkNotNull; 19 import static com.google.android.exoplayer2.util.Assertions.checkState; 20 21 import android.app.Activity; 22 import android.content.DialogInterface; 23 import android.content.Intent; 24 import android.net.Uri; 25 import android.os.Bundle; 26 import android.view.View; 27 import android.widget.ArrayAdapter; 28 import android.widget.Button; 29 import android.widget.CheckBox; 30 import android.widget.Spinner; 31 import android.widget.TextView; 32 import androidx.annotation.Nullable; 33 import androidx.appcompat.app.AlertDialog; 34 import androidx.appcompat.app.AppCompatActivity; 35 import com.google.android.exoplayer2.util.MimeTypes; 36 import com.google.android.exoplayer2.util.Util; 37 import com.google.android.material.slider.RangeSlider; 38 import com.google.android.material.slider.Slider; 39 import java.util.Arrays; 40 import java.util.List; 41 import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 42 import org.checkerframework.checker.nullness.qual.RequiresNonNull; 43 44 /** 45 * An {@link Activity} that sets the configuration to use for transforming and playing media, using 46 * {@link TransformerActivity}. 47 */ 48 public final class ConfigurationActivity extends AppCompatActivity { 49 public static final String SHOULD_REMOVE_AUDIO = "should_remove_audio"; 50 public static final String SHOULD_REMOVE_VIDEO = "should_remove_video"; 51 public static final String SHOULD_FLATTEN_FOR_SLOW_MOTION = "should_flatten_for_slow_motion"; 52 public static final String AUDIO_MIME_TYPE = "audio_mime_type"; 53 public static final String VIDEO_MIME_TYPE = "video_mime_type"; 54 public static final String RESOLUTION_HEIGHT = "resolution_height"; 55 public static final String SCALE_X = "scale_x"; 56 public static final String SCALE_Y = "scale_y"; 57 public static final String ROTATE_DEGREES = "rotate_degrees"; 58 public static final String ENABLE_FALLBACK = "enable_fallback"; 59 public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping"; 60 public static final String ENABLE_HDR_EDITING = "enable_hdr_editing"; 61 public static final String DEMO_FRAME_PROCESSORS_SELECTIONS = "demo_frame_processors_selections"; 62 public static final String PERIODIC_VIGNETTE_CENTER_X = "periodic_vignette_center_x"; 63 public static final String PERIODIC_VIGNETTE_CENTER_Y = "periodic_vignette_center_y"; 64 public static final String PERIODIC_VIGNETTE_INNER_RADIUS = "periodic_vignette_inner_radius"; 65 public static final String PERIODIC_VIGNETTE_OUTER_RADIUS = "periodic_vignette_outer_radius"; 66 private static final String[] INPUT_URIS = { 67 "https://html5demos.com/assets/dizzy.mp4", 68 "https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4", 69 "https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4", 70 "https://html5demos.com/assets/dizzy.webm", 71 "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_4k60.mp4", 72 "https://storage.googleapis.com/exoplayer-test-media-1/mp4/8k24fps_4s.mp4", 73 "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_avc_aac.mp4", 74 "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_rotated_avc_aac.mp4", 75 "https://storage.googleapis.com/exoplayer-test-media-1/mp4/slow-motion/slowMotion_stopwatch_240fps_long.mp4", 76 "https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-hdr-hdr10.mp4", 77 }; 78 private static final String[] URI_DESCRIPTIONS = { // same order as INPUT_URIS 79 "MP4 with H264 video and AAC audio", 80 "MP4 with H265 video and AAC audio", 81 "Long MP4 with H264 video and AAC audio", 82 "WebM with VP8 video and Vorbis audio", 83 "4K 60fps MP4 with H264 video and AAC audio (portrait, timestamps always increase)", 84 "8k 24fps MP4 with H265 video and AAC audio", 85 "MP4 with H264 video and AAC audio (portrait, H > W, 0\u00B0)", 86 "MP4 with H264 video and AAC audio (portrait, H < W, 90\u00B0)", 87 "SEF slow motion with 240 fps", 88 "MP4 with HDR (HDR10) H265 video (encoding may fail)", 89 }; 90 private static final String[] DEMO_FRAME_PROCESSORS = { 91 "Dizzy crop", "Periodic vignette", "3D spin", "Overlay logo & timer", "Zoom in start" 92 }; 93 private static final int PERIODIC_VIGNETTE_INDEX = 1; 94 private static final String SAME_AS_INPUT_OPTION = "same as input"; 95 private static final float HALF_DIAGONAL = 1f / (float) Math.sqrt(2); 96 97 private @MonotonicNonNull Button selectFileButton; 98 private @MonotonicNonNull TextView selectedFileTextView; 99 private @MonotonicNonNull CheckBox removeAudioCheckbox; 100 private @MonotonicNonNull CheckBox removeVideoCheckbox; 101 private @MonotonicNonNull CheckBox flattenForSlowMotionCheckbox; 102 private @MonotonicNonNull Spinner audioMimeSpinner; 103 private @MonotonicNonNull Spinner videoMimeSpinner; 104 private @MonotonicNonNull Spinner resolutionHeightSpinner; 105 private @MonotonicNonNull Spinner scaleSpinner; 106 private @MonotonicNonNull Spinner rotateSpinner; 107 private @MonotonicNonNull CheckBox enableFallbackCheckBox; 108 private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox; 109 private @MonotonicNonNull CheckBox enableHdrEditingCheckBox; 110 private @MonotonicNonNull Button selectDemoFrameProcessorsButton; 111 private boolean @MonotonicNonNull [] demoFrameProcessorsSelections; 112 private int inputUriPosition; 113 private float periodicVignetteCenterX; 114 private float periodicVignetteCenterY; 115 private float periodicVignetteInnerRadius; 116 private float periodicVignetteOuterRadius; 117 118 @Override onCreate(@ullable Bundle savedInstanceState)119 protected void onCreate(@Nullable Bundle savedInstanceState) { 120 super.onCreate(savedInstanceState); 121 setContentView(R.layout.configuration_activity); 122 123 findViewById(R.id.transform_button).setOnClickListener(this::startTransformation); 124 125 selectFileButton = findViewById(R.id.select_file_button); 126 selectFileButton.setOnClickListener(this::selectFile); 127 128 selectedFileTextView = findViewById(R.id.selected_file_text_view); 129 selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); 130 131 removeAudioCheckbox = findViewById(R.id.remove_audio_checkbox); 132 removeAudioCheckbox.setOnClickListener(this::onRemoveAudio); 133 134 removeVideoCheckbox = findViewById(R.id.remove_video_checkbox); 135 removeVideoCheckbox.setOnClickListener(this::onRemoveVideo); 136 137 flattenForSlowMotionCheckbox = findViewById(R.id.flatten_for_slow_motion_checkbox); 138 139 ArrayAdapter<String> audioMimeAdapter = 140 new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); 141 audioMimeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 142 audioMimeSpinner = findViewById(R.id.audio_mime_spinner); 143 audioMimeSpinner.setAdapter(audioMimeAdapter); 144 audioMimeAdapter.addAll( 145 SAME_AS_INPUT_OPTION, MimeTypes.AUDIO_AAC, MimeTypes.AUDIO_AMR_NB, MimeTypes.AUDIO_AMR_WB); 146 147 ArrayAdapter<String> videoMimeAdapter = 148 new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); 149 videoMimeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 150 videoMimeSpinner = findViewById(R.id.video_mime_spinner); 151 videoMimeSpinner.setAdapter(videoMimeAdapter); 152 videoMimeAdapter.addAll( 153 SAME_AS_INPUT_OPTION, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_H264, MimeTypes.VIDEO_MP4V); 154 if (Util.SDK_INT >= 24) { 155 videoMimeAdapter.add(MimeTypes.VIDEO_H265); 156 } 157 158 ArrayAdapter<String> resolutionHeightAdapter = 159 new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); 160 resolutionHeightAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 161 resolutionHeightSpinner = findViewById(R.id.resolution_height_spinner); 162 resolutionHeightSpinner.setAdapter(resolutionHeightAdapter); 163 resolutionHeightAdapter.addAll( 164 SAME_AS_INPUT_OPTION, "144", "240", "360", "480", "720", "1080", "1440", "2160"); 165 166 ArrayAdapter<String> scaleAdapter = 167 new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); 168 scaleAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 169 scaleSpinner = findViewById(R.id.scale_spinner); 170 scaleSpinner.setAdapter(scaleAdapter); 171 scaleAdapter.addAll(SAME_AS_INPUT_OPTION, "-1, -1", "-1, 1", "1, 1", ".5, 1", ".5, .5", "2, 2"); 172 173 ArrayAdapter<String> rotateAdapter = 174 new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); 175 rotateAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 176 rotateSpinner = findViewById(R.id.rotate_spinner); 177 rotateSpinner.setAdapter(rotateAdapter); 178 rotateAdapter.addAll(SAME_AS_INPUT_OPTION, "0", "10", "45", "60", "90", "180"); 179 180 enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox); 181 enableRequestSdrToneMappingCheckBox = findViewById(R.id.request_sdr_tone_mapping_checkbox); 182 enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported()); 183 findViewById(R.id.request_sdr_tone_mapping).setEnabled(isRequestSdrToneMappingSupported()); 184 enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox); 185 186 demoFrameProcessorsSelections = new boolean[DEMO_FRAME_PROCESSORS.length]; 187 selectDemoFrameProcessorsButton = findViewById(R.id.select_demo_frameprocessors_button); 188 selectDemoFrameProcessorsButton.setOnClickListener(this::selectFrameProcessors); 189 } 190 191 @Override onResume()192 protected void onResume() { 193 super.onResume(); 194 @Nullable Uri intentUri = getIntent().getData(); 195 if (intentUri != null) { 196 checkNotNull(selectFileButton).setEnabled(false); 197 checkNotNull(selectedFileTextView).setText(intentUri.toString()); 198 } 199 } 200 201 @Override onNewIntent(Intent intent)202 protected void onNewIntent(Intent intent) { 203 super.onNewIntent(intent); 204 setIntent(intent); 205 } 206 207 @RequiresNonNull({ 208 "removeAudioCheckbox", 209 "removeVideoCheckbox", 210 "flattenForSlowMotionCheckbox", 211 "audioMimeSpinner", 212 "videoMimeSpinner", 213 "resolutionHeightSpinner", 214 "scaleSpinner", 215 "rotateSpinner", 216 "enableFallbackCheckBox", 217 "enableRequestSdrToneMappingCheckBox", 218 "enableHdrEditingCheckBox", 219 "demoFrameProcessorsSelections" 220 }) startTransformation(View view)221 private void startTransformation(View view) { 222 Intent transformerIntent = new Intent(this, TransformerActivity.class); 223 Bundle bundle = new Bundle(); 224 bundle.putBoolean(SHOULD_REMOVE_AUDIO, removeAudioCheckbox.isChecked()); 225 bundle.putBoolean(SHOULD_REMOVE_VIDEO, removeVideoCheckbox.isChecked()); 226 bundle.putBoolean(SHOULD_FLATTEN_FOR_SLOW_MOTION, flattenForSlowMotionCheckbox.isChecked()); 227 String selectedAudioMimeType = String.valueOf(audioMimeSpinner.getSelectedItem()); 228 if (!SAME_AS_INPUT_OPTION.equals(selectedAudioMimeType)) { 229 bundle.putString(AUDIO_MIME_TYPE, selectedAudioMimeType); 230 } 231 String selectedVideoMimeType = String.valueOf(videoMimeSpinner.getSelectedItem()); 232 if (!SAME_AS_INPUT_OPTION.equals(selectedVideoMimeType)) { 233 bundle.putString(VIDEO_MIME_TYPE, selectedVideoMimeType); 234 } 235 String selectedResolutionHeight = String.valueOf(resolutionHeightSpinner.getSelectedItem()); 236 if (!SAME_AS_INPUT_OPTION.equals(selectedResolutionHeight)) { 237 bundle.putInt(RESOLUTION_HEIGHT, Integer.parseInt(selectedResolutionHeight)); 238 } 239 String selectedScale = String.valueOf(scaleSpinner.getSelectedItem()); 240 if (!SAME_AS_INPUT_OPTION.equals(selectedScale)) { 241 List<String> scaleXY = Arrays.asList(selectedScale.split(", ")); 242 checkState(scaleXY.size() == 2); 243 bundle.putFloat(SCALE_X, Float.parseFloat(scaleXY.get(0))); 244 bundle.putFloat(SCALE_Y, Float.parseFloat(scaleXY.get(1))); 245 } 246 String selectedRotate = String.valueOf(rotateSpinner.getSelectedItem()); 247 if (!SAME_AS_INPUT_OPTION.equals(selectedRotate)) { 248 bundle.putFloat(ROTATE_DEGREES, Float.parseFloat(selectedRotate)); 249 } 250 bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked()); 251 bundle.putBoolean( 252 ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked()); 253 bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked()); 254 bundle.putBooleanArray(DEMO_FRAME_PROCESSORS_SELECTIONS, demoFrameProcessorsSelections); 255 bundle.putFloat(PERIODIC_VIGNETTE_CENTER_X, periodicVignetteCenterX); 256 bundle.putFloat(PERIODIC_VIGNETTE_CENTER_Y, periodicVignetteCenterY); 257 bundle.putFloat(PERIODIC_VIGNETTE_INNER_RADIUS, periodicVignetteInnerRadius); 258 bundle.putFloat(PERIODIC_VIGNETTE_OUTER_RADIUS, periodicVignetteOuterRadius); 259 transformerIntent.putExtras(bundle); 260 261 @Nullable Uri intentUri = getIntent().getData(); 262 transformerIntent.setData( 263 intentUri != null ? intentUri : Uri.parse(INPUT_URIS[inputUriPosition])); 264 265 startActivity(transformerIntent); 266 } 267 selectFile(View view)268 private void selectFile(View view) { 269 new AlertDialog.Builder(/* context= */ this) 270 .setTitle(R.string.select_file_title) 271 .setSingleChoiceItems(URI_DESCRIPTIONS, inputUriPosition, this::selectFileInDialog) 272 .setPositiveButton(android.R.string.ok, /* listener= */ null) 273 .create() 274 .show(); 275 } 276 selectFrameProcessors(View view)277 private void selectFrameProcessors(View view) { 278 new AlertDialog.Builder(/* context= */ this) 279 .setTitle(R.string.select_demo_frameprocessors) 280 .setMultiChoiceItems( 281 DEMO_FRAME_PROCESSORS, 282 checkNotNull(demoFrameProcessorsSelections), 283 this::selectFrameProcessor) 284 .setPositiveButton(android.R.string.ok, /* listener= */ null) 285 .create() 286 .show(); 287 } 288 289 @RequiresNonNull("selectedFileTextView") selectFileInDialog(DialogInterface dialog, int which)290 private void selectFileInDialog(DialogInterface dialog, int which) { 291 inputUriPosition = which; 292 selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); 293 } 294 295 @RequiresNonNull("demoFrameProcessorsSelections") selectFrameProcessor(DialogInterface dialog, int which, boolean isChecked)296 private void selectFrameProcessor(DialogInterface dialog, int which, boolean isChecked) { 297 demoFrameProcessorsSelections[which] = isChecked; 298 if (!isChecked || which != PERIODIC_VIGNETTE_INDEX) { 299 return; 300 } 301 302 View dialogView = 303 getLayoutInflater().inflate(R.layout.periodic_vignette_options, /* root= */ null); 304 Slider centerXSlider = 305 checkNotNull(dialogView.findViewById(R.id.periodic_vignette_center_x_slider)); 306 Slider centerYSlider = 307 checkNotNull(dialogView.findViewById(R.id.periodic_vignette_center_y_slider)); 308 RangeSlider radiusRangeSlider = 309 checkNotNull(dialogView.findViewById(R.id.periodic_vignette_radius_range_slider)); 310 radiusRangeSlider.setValues(0f, HALF_DIAGONAL); 311 new AlertDialog.Builder(/* context= */ this) 312 .setTitle(R.string.periodic_vignette_options) 313 .setView(dialogView) 314 .setPositiveButton( 315 android.R.string.ok, 316 (DialogInterface dialogInterface, int i) -> { 317 periodicVignetteCenterX = centerXSlider.getValue(); 318 periodicVignetteCenterY = centerYSlider.getValue(); 319 List<Float> radiusRange = radiusRangeSlider.getValues(); 320 periodicVignetteInnerRadius = radiusRange.get(0); 321 periodicVignetteOuterRadius = radiusRange.get(1); 322 }) 323 .create() 324 .show(); 325 } 326 327 @RequiresNonNull({ 328 "removeVideoCheckbox", 329 "audioMimeSpinner", 330 "videoMimeSpinner", 331 "resolutionHeightSpinner", 332 "scaleSpinner", 333 "rotateSpinner", 334 "enableRequestSdrToneMappingCheckBox", 335 "enableHdrEditingCheckBox", 336 "selectDemoFrameProcessorsButton" 337 }) onRemoveAudio(View view)338 private void onRemoveAudio(View view) { 339 if (((CheckBox) view).isChecked()) { 340 removeVideoCheckbox.setChecked(false); 341 enableTrackSpecificOptions(/* isAudioEnabled= */ false, /* isVideoEnabled= */ true); 342 } else { 343 enableTrackSpecificOptions(/* isAudioEnabled= */ true, /* isVideoEnabled= */ true); 344 } 345 } 346 347 @RequiresNonNull({ 348 "removeAudioCheckbox", 349 "audioMimeSpinner", 350 "videoMimeSpinner", 351 "resolutionHeightSpinner", 352 "scaleSpinner", 353 "rotateSpinner", 354 "enableRequestSdrToneMappingCheckBox", 355 "enableHdrEditingCheckBox", 356 "selectDemoFrameProcessorsButton" 357 }) onRemoveVideo(View view)358 private void onRemoveVideo(View view) { 359 if (((CheckBox) view).isChecked()) { 360 removeAudioCheckbox.setChecked(false); 361 enableTrackSpecificOptions(/* isAudioEnabled= */ true, /* isVideoEnabled= */ false); 362 } else { 363 enableTrackSpecificOptions(/* isAudioEnabled= */ true, /* isVideoEnabled= */ true); 364 } 365 } 366 367 @RequiresNonNull({ 368 "audioMimeSpinner", 369 "videoMimeSpinner", 370 "resolutionHeightSpinner", 371 "scaleSpinner", 372 "rotateSpinner", 373 "enableRequestSdrToneMappingCheckBox", 374 "enableHdrEditingCheckBox", 375 "selectDemoFrameProcessorsButton" 376 }) enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled)377 private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) { 378 audioMimeSpinner.setEnabled(isAudioEnabled); 379 videoMimeSpinner.setEnabled(isVideoEnabled); 380 resolutionHeightSpinner.setEnabled(isVideoEnabled); 381 scaleSpinner.setEnabled(isVideoEnabled); 382 rotateSpinner.setEnabled(isVideoEnabled); 383 enableRequestSdrToneMappingCheckBox.setEnabled( 384 isRequestSdrToneMappingSupported() && isVideoEnabled); 385 enableHdrEditingCheckBox.setEnabled(isVideoEnabled); 386 selectDemoFrameProcessorsButton.setEnabled(isVideoEnabled); 387 388 findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled); 389 findViewById(R.id.video_mime_text_view).setEnabled(isVideoEnabled); 390 findViewById(R.id.resolution_height_text_view).setEnabled(isVideoEnabled); 391 findViewById(R.id.scale).setEnabled(isVideoEnabled); 392 findViewById(R.id.rotate).setEnabled(isVideoEnabled); 393 findViewById(R.id.request_sdr_tone_mapping) 394 .setEnabled(isRequestSdrToneMappingSupported() && isVideoEnabled); 395 findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled); 396 } 397 isRequestSdrToneMappingSupported()398 private static boolean isRequestSdrToneMappingSupported() { 399 return Util.SDK_INT >= 31; 400 } 401 } 402