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 android.Manifest.permission.READ_EXTERNAL_STORAGE; 19 import static com.google.android.exoplayer2.util.Assertions.checkNotNull; 20 21 import android.app.Activity; 22 import android.content.Intent; 23 import android.content.pm.PackageManager; 24 import android.net.Uri; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.view.SurfaceHolder; 28 import android.view.SurfaceView; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.widget.TextView; 32 import android.widget.Toast; 33 import androidx.annotation.Nullable; 34 import androidx.appcompat.app.AppCompatActivity; 35 import com.google.android.exoplayer2.C; 36 import com.google.android.exoplayer2.ExoPlayer; 37 import com.google.android.exoplayer2.MediaItem; 38 import com.google.android.exoplayer2.transformer.DefaultEncoderFactory; 39 import com.google.android.exoplayer2.transformer.EncoderSelector; 40 import com.google.android.exoplayer2.transformer.GlFrameProcessor; 41 import com.google.android.exoplayer2.transformer.ProgressHolder; 42 import com.google.android.exoplayer2.transformer.TransformationException; 43 import com.google.android.exoplayer2.transformer.TransformationRequest; 44 import com.google.android.exoplayer2.transformer.TransformationResult; 45 import com.google.android.exoplayer2.transformer.Transformer; 46 import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; 47 import com.google.android.exoplayer2.ui.StyledPlayerView; 48 import com.google.android.exoplayer2.util.DebugTextViewHelper; 49 import com.google.android.exoplayer2.util.Log; 50 import com.google.android.exoplayer2.util.Util; 51 import com.google.android.material.progressindicator.LinearProgressIndicator; 52 import com.google.common.base.Stopwatch; 53 import com.google.common.base.Ticker; 54 import com.google.common.collect.ImmutableList; 55 import java.io.File; 56 import java.io.IOException; 57 import java.util.concurrent.CountDownLatch; 58 import java.util.concurrent.TimeUnit; 59 import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 60 import org.checkerframework.checker.nullness.qual.RequiresNonNull; 61 62 /** An {@link Activity} that transforms and plays media using {@link Transformer}. */ 63 public final class TransformerActivity extends AppCompatActivity { 64 private static final String TAG = "TransformerActivity"; 65 66 private @MonotonicNonNull StyledPlayerView playerView; 67 private @MonotonicNonNull TextView debugTextView; 68 private @MonotonicNonNull TextView informationTextView; 69 private @MonotonicNonNull ViewGroup progressViewGroup; 70 private @MonotonicNonNull LinearProgressIndicator progressIndicator; 71 private @MonotonicNonNull Stopwatch transformationStopwatch; 72 private @MonotonicNonNull AspectRatioFrameLayout debugFrame; 73 74 @Nullable private DebugTextViewHelper debugTextViewHelper; 75 @Nullable private ExoPlayer player; 76 @Nullable private Transformer transformer; 77 @Nullable private File externalCacheFile; 78 79 @Override onCreate(@ullable Bundle savedInstanceState)80 protected void onCreate(@Nullable Bundle savedInstanceState) { 81 super.onCreate(savedInstanceState); 82 setContentView(R.layout.transformer_activity); 83 84 playerView = findViewById(R.id.player_view); 85 debugTextView = findViewById(R.id.debug_text_view); 86 informationTextView = findViewById(R.id.information_text_view); 87 progressViewGroup = findViewById(R.id.progress_view_group); 88 progressIndicator = findViewById(R.id.progress_indicator); 89 debugFrame = findViewById(R.id.debug_aspect_ratio_frame_layout); 90 91 transformationStopwatch = 92 Stopwatch.createUnstarted( 93 new Ticker() { 94 public long read() { 95 return android.os.SystemClock.elapsedRealtimeNanos(); 96 } 97 }); 98 } 99 100 @Override onStart()101 protected void onStart() { 102 super.onStart(); 103 104 checkNotNull(progressIndicator); 105 checkNotNull(informationTextView); 106 checkNotNull(transformationStopwatch); 107 checkNotNull(playerView); 108 checkNotNull(debugTextView); 109 checkNotNull(progressViewGroup); 110 checkNotNull(debugFrame); 111 startTransformation(); 112 113 playerView.onResume(); 114 } 115 116 @Override onStop()117 protected void onStop() { 118 super.onStop(); 119 120 checkNotNull(transformer).cancel(); 121 transformer = null; 122 123 // The stop watch is reset after cancelling the transformation, in case cancelling causes the 124 // stop watch to be stopped in a transformer callback. 125 checkNotNull(transformationStopwatch).reset(); 126 127 checkNotNull(playerView).onPause(); 128 releasePlayer(); 129 130 checkNotNull(externalCacheFile).delete(); 131 externalCacheFile = null; 132 } 133 134 @RequiresNonNull({ 135 "playerView", 136 "debugTextView", 137 "informationTextView", 138 "progressIndicator", 139 "transformationStopwatch", 140 "progressViewGroup", 141 "debugFrame", 142 }) startTransformation()143 private void startTransformation() { 144 requestTransformerPermission(); 145 146 Intent intent = getIntent(); 147 Uri uri = checkNotNull(intent.getData()); 148 try { 149 externalCacheFile = createExternalCacheFile("transformer-output.mp4"); 150 String filePath = externalCacheFile.getAbsolutePath(); 151 @Nullable Bundle bundle = intent.getExtras(); 152 Transformer transformer = createTransformer(bundle, filePath); 153 transformationStopwatch.start(); 154 transformer.startTransformation(MediaItem.fromUri(uri), filePath); 155 this.transformer = transformer; 156 } catch (IOException e) { 157 throw new IllegalStateException(e); 158 } 159 informationTextView.setText(R.string.transformation_started); 160 playerView.setVisibility(View.GONE); 161 Handler mainHandler = new Handler(getMainLooper()); 162 ProgressHolder progressHolder = new ProgressHolder(); 163 mainHandler.post( 164 new Runnable() { 165 @Override 166 public void run() { 167 if (transformer != null 168 && transformer.getProgress(progressHolder) 169 != Transformer.PROGRESS_STATE_NO_TRANSFORMATION) { 170 progressIndicator.setProgress(progressHolder.progress); 171 informationTextView.setText( 172 getString( 173 R.string.transformation_timer, 174 transformationStopwatch.elapsed(TimeUnit.SECONDS))); 175 mainHandler.postDelayed(/* r= */ this, /* delayMillis= */ 500); 176 } 177 } 178 }); 179 } 180 181 // Create a cache file, resetting it if it already exists. createExternalCacheFile(String fileName)182 private File createExternalCacheFile(String fileName) throws IOException { 183 File file = new File(getExternalCacheDir(), fileName); 184 if (file.exists() && !file.delete()) { 185 throw new IllegalStateException("Could not delete the previous transformer output file"); 186 } 187 if (!file.createNewFile()) { 188 throw new IllegalStateException("Could not create the transformer output file"); 189 } 190 return file; 191 } 192 193 @RequiresNonNull({ 194 "playerView", 195 "debugTextView", 196 "informationTextView", 197 "transformationStopwatch", 198 "progressViewGroup", 199 "debugFrame", 200 }) createTransformer(@ullable Bundle bundle, String filePath)201 private Transformer createTransformer(@Nullable Bundle bundle, String filePath) { 202 Transformer.Builder transformerBuilder = new Transformer.Builder(/* context= */ this); 203 if (bundle != null) { 204 TransformationRequest.Builder requestBuilder = new TransformationRequest.Builder(); 205 requestBuilder.setFlattenForSlowMotion( 206 bundle.getBoolean(ConfigurationActivity.SHOULD_FLATTEN_FOR_SLOW_MOTION)); 207 @Nullable String audioMimeType = bundle.getString(ConfigurationActivity.AUDIO_MIME_TYPE); 208 if (audioMimeType != null) { 209 requestBuilder.setAudioMimeType(audioMimeType); 210 } 211 @Nullable String videoMimeType = bundle.getString(ConfigurationActivity.VIDEO_MIME_TYPE); 212 if (videoMimeType != null) { 213 requestBuilder.setVideoMimeType(videoMimeType); 214 } 215 int resolutionHeight = 216 bundle.getInt( 217 ConfigurationActivity.RESOLUTION_HEIGHT, /* defaultValue= */ C.LENGTH_UNSET); 218 if (resolutionHeight != C.LENGTH_UNSET) { 219 requestBuilder.setResolution(resolutionHeight); 220 } 221 222 float scaleX = bundle.getFloat(ConfigurationActivity.SCALE_X, /* defaultValue= */ 1); 223 float scaleY = bundle.getFloat(ConfigurationActivity.SCALE_Y, /* defaultValue= */ 1); 224 requestBuilder.setScale(scaleX, scaleY); 225 226 float rotateDegrees = 227 bundle.getFloat(ConfigurationActivity.ROTATE_DEGREES, /* defaultValue= */ 0); 228 requestBuilder.setRotationDegrees(rotateDegrees); 229 230 requestBuilder.setEnableRequestSdrToneMapping( 231 bundle.getBoolean(ConfigurationActivity.ENABLE_REQUEST_SDR_TONE_MAPPING)); 232 requestBuilder.experimental_setEnableHdrEditing( 233 bundle.getBoolean(ConfigurationActivity.ENABLE_HDR_EDITING)); 234 transformerBuilder 235 .setTransformationRequest(requestBuilder.build()) 236 .setRemoveAudio(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_AUDIO)) 237 .setRemoveVideo(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_VIDEO)) 238 .setEncoderFactory( 239 new DefaultEncoderFactory( 240 EncoderSelector.DEFAULT, 241 /* enableFallback= */ bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK))); 242 243 ImmutableList.Builder<GlFrameProcessor> frameProcessors = new ImmutableList.Builder<>(); 244 @Nullable 245 boolean[] selectedFrameProcessors = 246 bundle.getBooleanArray(ConfigurationActivity.DEMO_FRAME_PROCESSORS_SELECTIONS); 247 if (selectedFrameProcessors != null) { 248 if (selectedFrameProcessors[0]) { 249 frameProcessors.add( 250 AdvancedFrameProcessorFactory.createDizzyCropFrameProcessor(/* context= */ this)); 251 } 252 if (selectedFrameProcessors[1]) { 253 frameProcessors.add( 254 new PeriodicVignetteFrameProcessor( 255 /* context= */ this, 256 bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_X), 257 bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_Y), 258 /* minInnerRadius= */ bundle.getFloat( 259 ConfigurationActivity.PERIODIC_VIGNETTE_INNER_RADIUS), 260 /* maxInnerRadius= */ bundle.getFloat( 261 ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS), 262 bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS))); 263 } 264 if (selectedFrameProcessors[2]) { 265 frameProcessors.add( 266 AdvancedFrameProcessorFactory.createSpin3dFrameProcessor(/* context= */ this)); 267 } 268 if (selectedFrameProcessors[3]) { 269 frameProcessors.add(new BitmapOverlayFrameProcessor(/* context= */ this)); 270 } 271 if (selectedFrameProcessors[4]) { 272 frameProcessors.add( 273 AdvancedFrameProcessorFactory.createZoomInTransitionFrameProcessor( 274 /* context= */ this)); 275 } 276 transformerBuilder.setFrameProcessors(frameProcessors.build()); 277 } 278 } 279 return transformerBuilder 280 .addListener( 281 new Transformer.Listener() { 282 @Override 283 public void onTransformationCompleted( 284 MediaItem mediaItem, TransformationResult transformationResult) { 285 TransformerActivity.this.onTransformationCompleted(filePath); 286 } 287 288 @Override 289 public void onTransformationError( 290 MediaItem mediaItem, TransformationException exception) { 291 TransformerActivity.this.onTransformationError(exception); 292 } 293 }) 294 .setDebugViewProvider(new DemoDebugViewProvider()) 295 .build(); 296 } 297 298 @RequiresNonNull({ 299 "informationTextView", 300 "progressViewGroup", 301 "debugFrame", 302 "transformationStopwatch", 303 }) 304 private void onTransformationError(TransformationException exception) { 305 transformationStopwatch.stop(); 306 informationTextView.setText(R.string.transformation_error); 307 progressViewGroup.setVisibility(View.GONE); 308 debugFrame.removeAllViews(); 309 Toast.makeText( 310 TransformerActivity.this, "Transformation error: " + exception, Toast.LENGTH_LONG) 311 .show(); 312 Log.e(TAG, "Transformation error", exception); 313 } 314 315 @RequiresNonNull({ 316 "playerView", 317 "debugTextView", 318 "informationTextView", 319 "progressViewGroup", 320 "debugFrame", 321 "transformationStopwatch", 322 }) 323 private void onTransformationCompleted(String filePath) { 324 transformationStopwatch.stop(); 325 informationTextView.setText( 326 getString( 327 R.string.transformation_completed, transformationStopwatch.elapsed(TimeUnit.SECONDS))); 328 progressViewGroup.setVisibility(View.GONE); 329 debugFrame.removeAllViews(); 330 playerView.setVisibility(View.VISIBLE); 331 playMediaItem(MediaItem.fromUri("file://" + filePath)); 332 Log.d(TAG, "Output file path: file://" + filePath); 333 } 334 335 @RequiresNonNull({"playerView", "debugTextView"}) 336 private void playMediaItem(MediaItem mediaItem) { 337 playerView.setPlayer(null); 338 releasePlayer(); 339 340 ExoPlayer player = new ExoPlayer.Builder(/* context= */ this).build(); 341 playerView.setPlayer(player); 342 player.setMediaItem(mediaItem); 343 player.play(); 344 player.prepare(); 345 this.player = player; 346 debugTextViewHelper = new DebugTextViewHelper(player, debugTextView); 347 debugTextViewHelper.start(); 348 } 349 350 private void releasePlayer() { 351 if (debugTextViewHelper != null) { 352 debugTextViewHelper.stop(); 353 debugTextViewHelper = null; 354 } 355 if (player != null) { 356 player.release(); 357 player = null; 358 } 359 } 360 361 private void requestTransformerPermission() { 362 if (Util.SDK_INT < 23) { 363 return; 364 } 365 if (checkSelfPermission(READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 366 requestPermissions(new String[] {READ_EXTERNAL_STORAGE}, /* requestCode= */ 0); 367 } 368 } 369 370 private final class DemoDebugViewProvider implements Transformer.DebugViewProvider { 371 372 @Nullable 373 @Override 374 public SurfaceView getDebugPreviewSurfaceView(int width, int height) { 375 // Update the UI on the main thread and wait for the output surface to be available. 376 CountDownLatch surfaceCreatedCountDownLatch = new CountDownLatch(1); 377 SurfaceView surfaceView = new SurfaceView(/* context= */ TransformerActivity.this); 378 runOnUiThread( 379 () -> { 380 AspectRatioFrameLayout debugFrame = checkNotNull(TransformerActivity.this.debugFrame); 381 debugFrame.addView(surfaceView); 382 debugFrame.setAspectRatio((float) width / height); 383 surfaceView 384 .getHolder() 385 .addCallback( 386 new SurfaceHolder.Callback() { 387 @Override 388 public void surfaceCreated(SurfaceHolder surfaceHolder) { 389 surfaceCreatedCountDownLatch.countDown(); 390 } 391 392 @Override 393 public void surfaceChanged( 394 SurfaceHolder surfaceHolder, int format, int width, int height) { 395 // Do nothing. 396 } 397 398 @Override 399 public void surfaceDestroyed(SurfaceHolder surfaceHolder) { 400 // Do nothing. 401 } 402 }); 403 }); 404 try { 405 surfaceCreatedCountDownLatch.await(); 406 } catch (InterruptedException e) { 407 Log.w(TAG, "Interrupted waiting for debug surface."); 408 Thread.currentThread().interrupt(); 409 return null; 410 } 411 return surfaceView; 412 } 413 } 414 } 415