• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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