• 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 
17 package com.google.android.exoplayer2.transformer;
18 
19 import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
20 import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;
21 import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
22 import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
23 import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
24 import static com.google.android.exoplayer2.util.Assertions.checkState;
25 import static java.lang.Math.min;
26 import static java.lang.annotation.ElementType.TYPE_USE;
27 
28 import android.content.Context;
29 import android.os.Handler;
30 import android.os.Looper;
31 import android.os.ParcelFileDescriptor;
32 import android.view.SurfaceView;
33 import androidx.annotation.IntDef;
34 import androidx.annotation.Nullable;
35 import androidx.annotation.RequiresApi;
36 import androidx.annotation.VisibleForTesting;
37 import com.google.android.exoplayer2.C;
38 import com.google.android.exoplayer2.DefaultLoadControl;
39 import com.google.android.exoplayer2.ExoPlayer;
40 import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
41 import com.google.android.exoplayer2.MediaItem;
42 import com.google.android.exoplayer2.PlaybackException;
43 import com.google.android.exoplayer2.Player;
44 import com.google.android.exoplayer2.Renderer;
45 import com.google.android.exoplayer2.RenderersFactory;
46 import com.google.android.exoplayer2.Timeline;
47 import com.google.android.exoplayer2.TracksInfo;
48 import com.google.android.exoplayer2.audio.AudioRendererEventListener;
49 import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
50 import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
51 import com.google.android.exoplayer2.metadata.MetadataOutput;
52 import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
53 import com.google.android.exoplayer2.source.MediaSource;
54 import com.google.android.exoplayer2.text.TextOutput;
55 import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
56 import com.google.android.exoplayer2.util.Clock;
57 import com.google.android.exoplayer2.util.ListenerSet;
58 import com.google.android.exoplayer2.util.MimeTypes;
59 import com.google.android.exoplayer2.util.Util;
60 import com.google.android.exoplayer2.video.VideoRendererEventListener;
61 import com.google.common.collect.ImmutableList;
62 import java.io.IOException;
63 import java.lang.annotation.Documented;
64 import java.lang.annotation.Retention;
65 import java.lang.annotation.RetentionPolicy;
66 import java.lang.annotation.Target;
67 import java.util.List;
68 import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
69 
70 /**
71  * A transformer to transform media inputs.
72  *
73  * <p>The same Transformer instance can be used to transform multiple inputs (sequentially, not
74  * concurrently).
75  *
76  * <p>Transformer instances must be accessed from a single application thread. For the vast majority
77  * of cases this should be the application's main thread. The thread on which a Transformer instance
78  * must be accessed can be explicitly specified by passing a {@link Looper} when creating the
79  * transformer. If no Looper is specified, then the Looper of the thread that the {@link
80  * Transformer.Builder} is created on is used, or if that thread does not have a Looper, the Looper
81  * of the application's main thread is used. In all cases the Looper of the thread from which the
82  * transformer must be accessed can be queried using {@link #getApplicationLooper()}.
83  */
84 public final class Transformer {
85 
86   static {
87     ExoPlayerLibraryInfo.registerModule("goog.exo.transformer");
88   }
89 
90   /** A builder for {@link Transformer} instances. */
91   public static final class Builder {
92 
93     // Mandatory field.
94     // TODO(huangdarwin): Update @MonotonicNonNull to final after deprecated {@link
95     // #setContext(Context)} is removed.
96     private @MonotonicNonNull Context context;
97 
98     // Optional fields.
99     private MediaSource.@MonotonicNonNull Factory mediaSourceFactory;
100     private Muxer.Factory muxerFactory;
101     private boolean removeAudio;
102     private boolean removeVideo;
103     private String containerMimeType;
104     private TransformationRequest transformationRequest;
105     private ImmutableList<GlFrameProcessor> frameProcessors;
106     private ListenerSet<Transformer.Listener> listeners;
107     private DebugViewProvider debugViewProvider;
108     private Looper looper;
109     private Clock clock;
110     private Codec.EncoderFactory encoderFactory;
111 
112     /**
113      * @deprecated Use {@link #Builder(Context)} instead.
114      */
115     @Deprecated
Builder()116     public Builder() {
117       muxerFactory = new FrameworkMuxer.Factory();
118       looper = Util.getCurrentOrMainLooper();
119       clock = Clock.DEFAULT;
120       listeners = new ListenerSet<>(looper, clock, (listener, flags) -> {});
121       encoderFactory = Codec.EncoderFactory.DEFAULT;
122       debugViewProvider = DebugViewProvider.NONE;
123       containerMimeType = MimeTypes.VIDEO_MP4;
124       transformationRequest = new TransformationRequest.Builder().build();
125       frameProcessors = ImmutableList.of();
126     }
127 
128     /**
129      * Creates a builder with default values.
130      *
131      * @param context The {@link Context}.
132      */
Builder(Context context)133     public Builder(Context context) {
134       this.context = context.getApplicationContext();
135       muxerFactory = new FrameworkMuxer.Factory();
136       looper = Util.getCurrentOrMainLooper();
137       clock = Clock.DEFAULT;
138       listeners = new ListenerSet<>(looper, clock, (listener, flags) -> {});
139       encoderFactory = Codec.EncoderFactory.DEFAULT;
140       debugViewProvider = DebugViewProvider.NONE;
141       containerMimeType = MimeTypes.VIDEO_MP4;
142       transformationRequest = new TransformationRequest.Builder().build();
143       frameProcessors = ImmutableList.of();
144     }
145 
146     /** Creates a builder with the values of the provided {@link Transformer}. */
Builder(Transformer transformer)147     private Builder(Transformer transformer) {
148       this.context = transformer.context;
149       this.mediaSourceFactory = transformer.mediaSourceFactory;
150       this.muxerFactory = transformer.muxerFactory;
151       this.removeAudio = transformer.removeAudio;
152       this.removeVideo = transformer.removeVideo;
153       this.containerMimeType = transformer.containerMimeType;
154       this.transformationRequest = transformer.transformationRequest;
155       this.frameProcessors = transformer.frameProcessors;
156       this.listeners = transformer.listeners;
157       this.looper = transformer.looper;
158       this.encoderFactory = transformer.encoderFactory;
159       this.debugViewProvider = transformer.debugViewProvider;
160       this.clock = transformer.clock;
161     }
162 
163     /**
164      * @deprecated Use {@link #Builder(Context)} instead.
165      */
166     @Deprecated
setContext(Context context)167     public Builder setContext(Context context) {
168       this.context = context.getApplicationContext();
169       return this;
170     }
171 
172     /**
173      * Sets the {@link TransformationRequest} which configures the editing and transcoding options.
174      *
175      * <p>Actual applied values may differ, per device capabilities. {@link
176      * Listener#onFallbackApplied(MediaItem, TransformationRequest, TransformationRequest)} will be
177      * invoked with the actual applied values.
178      *
179      * @param transformationRequest The {@link TransformationRequest}.
180      * @return This builder.
181      */
setTransformationRequest(TransformationRequest transformationRequest)182     public Builder setTransformationRequest(TransformationRequest transformationRequest) {
183       this.transformationRequest = transformationRequest;
184       return this;
185     }
186 
187     /**
188      * Sets the {@linkplain GlFrameProcessor frame processors} to apply to each frame.
189      *
190      * <p>The {@linkplain GlFrameProcessor frame processors} are applied before any {@linkplain
191      * TransformationRequest.Builder#setScale(float, float) scale}, {@linkplain
192      * TransformationRequest.Builder#setRotationDegrees(float) rotation}, or {@linkplain
193      * TransformationRequest.Builder#setResolution(int) resolution} changes specified in the {@link
194      * #setTransformationRequest(TransformationRequest) TransformationRequest} but after {@linkplain
195      * TransformationRequest.Builder#setFlattenForSlowMotion(boolean) slow-motion flattening}.
196      *
197      * @param frameProcessors The {@linkplain GlFrameProcessor frame processors}.
198      * @return This builder.
199      */
setFrameProcessors(List<GlFrameProcessor> frameProcessors)200     public Builder setFrameProcessors(List<GlFrameProcessor> frameProcessors) {
201       this.frameProcessors = ImmutableList.copyOf(frameProcessors);
202       return this;
203     }
204 
205     /**
206      * Sets the {@link MediaSource.Factory} to be used to retrieve the inputs to transform.
207      *
208      * <p>The default value is a {@link DefaultMediaSourceFactory} built with the context provided
209      * in {@linkplain #Builder(Context) the constructor}.
210      *
211      * @param mediaSourceFactory A {@link MediaSource.Factory}.
212      * @return This builder.
213      */
setMediaSourceFactory(MediaSource.Factory mediaSourceFactory)214     public Builder setMediaSourceFactory(MediaSource.Factory mediaSourceFactory) {
215       this.mediaSourceFactory = mediaSourceFactory;
216       return this;
217     }
218 
219     /**
220      * Sets whether to remove the audio from the output.
221      *
222      * <p>The default value is {@code false}.
223      *
224      * <p>The audio and video cannot both be removed because the output would not contain any
225      * samples.
226      *
227      * @param removeAudio Whether to remove the audio.
228      * @return This builder.
229      */
setRemoveAudio(boolean removeAudio)230     public Builder setRemoveAudio(boolean removeAudio) {
231       this.removeAudio = removeAudio;
232       return this;
233     }
234 
235     /**
236      * Sets whether to remove the video from the output.
237      *
238      * <p>The default value is {@code false}.
239      *
240      * <p>The audio and video cannot both be removed because the output would not contain any
241      * samples.
242      *
243      * @param removeVideo Whether to remove the video.
244      * @return This builder.
245      */
setRemoveVideo(boolean removeVideo)246     public Builder setRemoveVideo(boolean removeVideo) {
247       this.removeVideo = removeVideo;
248       return this;
249     }
250 
251     /**
252      * @deprecated Use {@link TransformationRequest.Builder#setFlattenForSlowMotion(boolean)}
253      *     instead.
254      */
255     @Deprecated
setFlattenForSlowMotion(boolean flattenForSlowMotion)256     public Builder setFlattenForSlowMotion(boolean flattenForSlowMotion) {
257       transformationRequest =
258           transformationRequest.buildUpon().setFlattenForSlowMotion(flattenForSlowMotion).build();
259       return this;
260     }
261 
262     /**
263      * @deprecated This feature will be removed in a following release and the MIME type of the
264      *     output will always be MP4.
265      */
266     @Deprecated
setOutputMimeType(String outputMimeType)267     public Builder setOutputMimeType(String outputMimeType) {
268       this.containerMimeType = outputMimeType;
269       return this;
270     }
271 
272     /**
273      * @deprecated Use {@link #addListener(Listener)}, {@link #removeListener(Listener)} or {@link
274      *     #removeAllListeners()} instead.
275      */
276     @Deprecated
setListener(Transformer.Listener listener)277     public Builder setListener(Transformer.Listener listener) {
278       this.listeners.clear();
279       this.listeners.add(listener);
280       return this;
281     }
282 
283     /**
284      * Adds a {@link Transformer.Listener} to listen to the transformation events.
285      *
286      * <p>This is equivalent to {@link Transformer#addListener(Listener)}.
287      *
288      * @param listener A {@link Transformer.Listener}.
289      * @return This builder.
290      */
addListener(Transformer.Listener listener)291     public Builder addListener(Transformer.Listener listener) {
292       this.listeners.add(listener);
293       return this;
294     }
295 
296     /**
297      * Removes a {@link Transformer.Listener}.
298      *
299      * <p>This is equivalent to {@link Transformer#removeListener(Listener)}.
300      *
301      * @param listener A {@link Transformer.Listener}.
302      * @return This builder.
303      */
removeListener(Transformer.Listener listener)304     public Builder removeListener(Transformer.Listener listener) {
305       this.listeners.remove(listener);
306       return this;
307     }
308 
309     /**
310      * Removes all {@linkplain Transformer.Listener listeners}.
311      *
312      * <p>This is equivalent to {@link Transformer#removeAllListeners()}.
313      *
314      * @return This builder.
315      */
removeAllListeners()316     public Builder removeAllListeners() {
317       this.listeners.clear();
318       return this;
319     }
320 
321     /**
322      * Sets the {@link Looper} that must be used for all calls to the transformer and that is used
323      * to call listeners on.
324      *
325      * <p>The default value is the Looper of the thread that this builder was created on, or if that
326      * thread does not have a Looper, the Looper of the application's main thread.
327      *
328      * @param looper A {@link Looper}.
329      * @return This builder.
330      */
setLooper(Looper looper)331     public Builder setLooper(Looper looper) {
332       this.looper = looper;
333       this.listeners = listeners.copy(looper, (listener, flags) -> {});
334       return this;
335     }
336 
337     /**
338      * Sets the {@link Codec.EncoderFactory} that will be used by the transformer.
339      *
340      * <p>The default value is {@link Codec.EncoderFactory#DEFAULT}.
341      *
342      * @param encoderFactory The {@link Codec.EncoderFactory} instance.
343      * @return This builder.
344      */
setEncoderFactory(Codec.EncoderFactory encoderFactory)345     public Builder setEncoderFactory(Codec.EncoderFactory encoderFactory) {
346       this.encoderFactory = encoderFactory;
347       return this;
348     }
349 
350     /**
351      * Sets a provider for views to show diagnostic information (if available) during
352      * transformation.
353      *
354      * <p>This is intended for debugging. The default value is {@link DebugViewProvider#NONE}, which
355      * doesn't show any debug info.
356      *
357      * <p>Not all transformations will result in debug views being populated.
358      *
359      * @param debugViewProvider Provider for debug views.
360      * @return This builder.
361      */
setDebugViewProvider(DebugViewProvider debugViewProvider)362     public Builder setDebugViewProvider(DebugViewProvider debugViewProvider) {
363       this.debugViewProvider = debugViewProvider;
364       return this;
365     }
366 
367     /**
368      * Sets the {@link Clock} that will be used by the transformer.
369      *
370      * <p>The default value is {@link Clock#DEFAULT}.
371      *
372      * @param clock The {@link Clock} instance.
373      * @return This builder.
374      */
375     @VisibleForTesting
setClock(Clock clock)376     /* package */ Builder setClock(Clock clock) {
377       this.clock = clock;
378       this.listeners = listeners.copy(looper, clock, (listener, flags) -> {});
379       return this;
380     }
381 
382     /**
383      * Sets the factory for muxers that write the media container.
384      *
385      * <p>The default value is a {@link FrameworkMuxer.Factory}.
386      *
387      * @param muxerFactory A {@link Muxer.Factory}.
388      * @return This builder.
389      */
390     @VisibleForTesting
setMuxerFactory(Muxer.Factory muxerFactory)391     /* package */ Builder setMuxerFactory(Muxer.Factory muxerFactory) {
392       this.muxerFactory = muxerFactory;
393       return this;
394     }
395 
396     /**
397      * Builds a {@link Transformer} instance.
398      *
399      * @throws NullPointerException If the {@link Context} has not been provided.
400      * @throws IllegalStateException If both audio and video have been removed (otherwise the output
401      *     would not contain any samples).
402      * @throws IllegalStateException If the muxer doesn't support the requested audio MIME type.
403      * @throws IllegalStateException If the muxer doesn't support the requested video MIME type.
404      */
build()405     public Transformer build() {
406       // TODO(huangdarwin): Remove this checkNotNull after deprecated {@link #setContext(Context)}
407       // is removed.
408       checkNotNull(context);
409       if (mediaSourceFactory == null) {
410         DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory();
411         if (transformationRequest.flattenForSlowMotion) {
412           defaultExtractorsFactory.setMp4ExtractorFlags(Mp4Extractor.FLAG_READ_SEF_DATA);
413         }
414         mediaSourceFactory = new DefaultMediaSourceFactory(context, defaultExtractorsFactory);
415       }
416       checkState(
417           muxerFactory.supportsOutputMimeType(containerMimeType),
418           "Unsupported container MIME type: " + containerMimeType);
419       if (transformationRequest.audioMimeType != null) {
420         checkSampleMimeType(transformationRequest.audioMimeType);
421       }
422       if (transformationRequest.videoMimeType != null) {
423         checkSampleMimeType(transformationRequest.videoMimeType);
424       }
425       return new Transformer(
426           context,
427           mediaSourceFactory,
428           muxerFactory,
429           removeAudio,
430           removeVideo,
431           containerMimeType,
432           transformationRequest,
433           frameProcessors,
434           listeners,
435           looper,
436           clock,
437           encoderFactory,
438           Codec.DecoderFactory.DEFAULT,
439           debugViewProvider);
440     }
441 
checkSampleMimeType(String sampleMimeType)442     private void checkSampleMimeType(String sampleMimeType) {
443       checkState(
444           muxerFactory.supportsSampleMimeType(sampleMimeType, containerMimeType),
445           "Unsupported sample MIME type "
446               + sampleMimeType
447               + " for container MIME type "
448               + containerMimeType);
449     }
450   }
451 
452   /** A listener for the transformation events. */
453   public interface Listener {
454 
455     /**
456      * @deprecated Use {@link #onTransformationCompleted(MediaItem, TransformationResult)} instead.
457      */
458     @Deprecated
onTransformationCompleted(MediaItem inputMediaItem)459     default void onTransformationCompleted(MediaItem inputMediaItem) {}
460 
461     /**
462      * Called when the transformation is completed successfully.
463      *
464      * @param inputMediaItem The {@link MediaItem} for which the transformation is completed.
465      * @param transformationResult The {@link TransformationResult} of the transformation.
466      */
onTransformationCompleted( MediaItem inputMediaItem, TransformationResult transformationResult)467     default void onTransformationCompleted(
468         MediaItem inputMediaItem, TransformationResult transformationResult) {
469       onTransformationCompleted(inputMediaItem);
470     }
471 
472     /**
473      * @deprecated Use {@link #onTransformationError(MediaItem, TransformationException)}.
474      */
475     @Deprecated
onTransformationError(MediaItem inputMediaItem, Exception exception)476     default void onTransformationError(MediaItem inputMediaItem, Exception exception) {
477       onTransformationError(inputMediaItem, (TransformationException) exception);
478     }
479 
480     /**
481      * Called if an exception occurs during the transformation.
482      *
483      * @param inputMediaItem The {@link MediaItem} for which the exception occurs.
484      * @param exception The {@link TransformationException} describing the exception.
485      */
onTransformationError( MediaItem inputMediaItem, TransformationException exception)486     default void onTransformationError(
487         MediaItem inputMediaItem, TransformationException exception) {}
488 
489     /**
490      * Called when fallback to an alternative {@link TransformationRequest} is necessary to comply
491      * with muxer or device constraints.
492      *
493      * @param inputMediaItem The {@link MediaItem} for which the transformation is requested.
494      * @param originalTransformationRequest The unsupported {@link TransformationRequest} used when
495      *     building {@link Transformer}.
496      * @param fallbackTransformationRequest The alternative {@link TransformationRequest}.
497      */
onFallbackApplied( MediaItem inputMediaItem, TransformationRequest originalTransformationRequest, TransformationRequest fallbackTransformationRequest)498     default void onFallbackApplied(
499         MediaItem inputMediaItem,
500         TransformationRequest originalTransformationRequest,
501         TransformationRequest fallbackTransformationRequest) {}
502   }
503 
504   /** Provider for views to show diagnostic information during transformation, for debugging. */
505   public interface DebugViewProvider {
506 
507     /** Debug view provider that doesn't show any debug info. */
508     DebugViewProvider NONE = (int width, int height) -> null;
509 
510     /**
511      * Returns a new surface view to show a preview of transformer output with the given
512      * width/height in pixels, or {@code null} if no debug information should be shown.
513      *
514      * <p>This method may be called on an arbitrary thread.
515      */
516     @Nullable
getDebugPreviewSurfaceView(int width, int height)517     SurfaceView getDebugPreviewSurfaceView(int width, int height);
518   }
519 
520   /**
521    * Progress state. One of {@link #PROGRESS_STATE_WAITING_FOR_AVAILABILITY}, {@link
522    * #PROGRESS_STATE_AVAILABLE}, {@link #PROGRESS_STATE_UNAVAILABLE}, {@link
523    * #PROGRESS_STATE_NO_TRANSFORMATION}
524    */
525   @Documented
526   @Retention(RetentionPolicy.SOURCE)
527   @Target(TYPE_USE)
528   @IntDef({
529     PROGRESS_STATE_WAITING_FOR_AVAILABILITY,
530     PROGRESS_STATE_AVAILABLE,
531     PROGRESS_STATE_UNAVAILABLE,
532     PROGRESS_STATE_NO_TRANSFORMATION
533   })
534   public @interface ProgressState {}
535 
536   /**
537    * Indicates that the progress is unavailable for the current transformation, but might become
538    * available.
539    */
540   public static final int PROGRESS_STATE_WAITING_FOR_AVAILABILITY = 0;
541   /** Indicates that the progress is available. */
542   public static final int PROGRESS_STATE_AVAILABLE = 1;
543   /** Indicates that the progress is permanently unavailable for the current transformation. */
544   public static final int PROGRESS_STATE_UNAVAILABLE = 2;
545   /** Indicates that there is no current transformation. */
546   public static final int PROGRESS_STATE_NO_TRANSFORMATION = 4;
547 
548   private final Context context;
549   private final MediaSource.Factory mediaSourceFactory;
550   private final Muxer.Factory muxerFactory;
551   private final boolean removeAudio;
552   private final boolean removeVideo;
553   private final String containerMimeType;
554   private final TransformationRequest transformationRequest;
555   private final ImmutableList<GlFrameProcessor> frameProcessors;
556   private final Looper looper;
557   private final Clock clock;
558   private final Codec.EncoderFactory encoderFactory;
559   private final Codec.DecoderFactory decoderFactory;
560   private final Transformer.DebugViewProvider debugViewProvider;
561   private final ListenerSet<Transformer.Listener> listeners;
562 
563   @Nullable private MuxerWrapper muxerWrapper;
564   @Nullable private ExoPlayer player;
565   private @ProgressState int progressState;
566   private boolean isCancelling;
567 
Transformer( Context context, MediaSource.Factory mediaSourceFactory, Muxer.Factory muxerFactory, boolean removeAudio, boolean removeVideo, String containerMimeType, TransformationRequest transformationRequest, ImmutableList<GlFrameProcessor> frameProcessors, ListenerSet<Transformer.Listener> listeners, Looper looper, Clock clock, Codec.EncoderFactory encoderFactory, Codec.DecoderFactory decoderFactory, Transformer.DebugViewProvider debugViewProvider)568   private Transformer(
569       Context context,
570       MediaSource.Factory mediaSourceFactory,
571       Muxer.Factory muxerFactory,
572       boolean removeAudio,
573       boolean removeVideo,
574       String containerMimeType,
575       TransformationRequest transformationRequest,
576       ImmutableList<GlFrameProcessor> frameProcessors,
577       ListenerSet<Transformer.Listener> listeners,
578       Looper looper,
579       Clock clock,
580       Codec.EncoderFactory encoderFactory,
581       Codec.DecoderFactory decoderFactory,
582       Transformer.DebugViewProvider debugViewProvider) {
583     checkState(!removeAudio || !removeVideo, "Audio and video cannot both be removed.");
584     this.context = context;
585     this.mediaSourceFactory = mediaSourceFactory;
586     this.muxerFactory = muxerFactory;
587     this.removeAudio = removeAudio;
588     this.removeVideo = removeVideo;
589     this.containerMimeType = containerMimeType;
590     this.transformationRequest = transformationRequest;
591     this.frameProcessors = frameProcessors;
592     this.listeners = listeners;
593     this.looper = looper;
594     this.clock = clock;
595     this.encoderFactory = encoderFactory;
596     this.decoderFactory = decoderFactory;
597     this.debugViewProvider = debugViewProvider;
598     progressState = PROGRESS_STATE_NO_TRANSFORMATION;
599   }
600 
601   /** Returns a {@link Transformer.Builder} initialized with the values of this instance. */
buildUpon()602   public Builder buildUpon() {
603     return new Builder(this);
604   }
605 
606   /**
607    * @deprecated Use {@link #addListener(Listener)}, {@link #removeListener(Listener)} or {@link
608    *     #removeAllListeners()} instead.
609    */
610   @Deprecated
setListener(Transformer.Listener listener)611   public void setListener(Transformer.Listener listener) {
612     verifyApplicationThread();
613     this.listeners.clear();
614     this.listeners.add(listener);
615   }
616 
617   /**
618    * Adds a {@link Transformer.Listener} to listen to the transformation events.
619    *
620    * @param listener A {@link Transformer.Listener}.
621    * @throws IllegalStateException If this method is called from the wrong thread.
622    */
addListener(Transformer.Listener listener)623   public void addListener(Transformer.Listener listener) {
624     verifyApplicationThread();
625     this.listeners.add(listener);
626   }
627 
628   /**
629    * Removes a {@link Transformer.Listener}.
630    *
631    * @param listener A {@link Transformer.Listener}.
632    * @throws IllegalStateException If this method is called from the wrong thread.
633    */
removeListener(Transformer.Listener listener)634   public void removeListener(Transformer.Listener listener) {
635     verifyApplicationThread();
636     this.listeners.remove(listener);
637   }
638 
639   /**
640    * Removes all {@linkplain Transformer.Listener listeners}.
641    *
642    * @throws IllegalStateException If this method is called from the wrong thread.
643    */
removeAllListeners()644   public void removeAllListeners() {
645     verifyApplicationThread();
646     this.listeners.clear();
647   }
648 
649   /**
650    * Starts an asynchronous operation to transform the given {@link MediaItem}.
651    *
652    * <p>The transformation state is notified through the {@linkplain Builder#addListener(Listener)
653    * listener}.
654    *
655    * <p>Concurrent transformations on the same Transformer object are not allowed.
656    *
657    * <p>The output is an MP4 file. It can contain at most one video track and one audio track. Other
658    * track types are ignored. For adaptive bitrate {@linkplain MediaSource media sources}, the
659    * highest bitrate video and audio streams are selected.
660    *
661    * @param mediaItem The {@link MediaItem} to transform.
662    * @param path The path to the output file.
663    * @throws IllegalArgumentException If the path is invalid.
664    * @throws IllegalStateException If this method is called from the wrong thread.
665    * @throws IllegalStateException If a transformation is already in progress.
666    * @throws IOException If an error occurs opening the output file for writing.
667    */
startTransformation(MediaItem mediaItem, String path)668   public void startTransformation(MediaItem mediaItem, String path) throws IOException {
669     startTransformation(mediaItem, muxerFactory.create(path, containerMimeType));
670   }
671 
672   /**
673    * Starts an asynchronous operation to transform the given {@link MediaItem}.
674    *
675    * <p>The transformation state is notified through the {@linkplain Builder#addListener(Listener)
676    * listener}.
677    *
678    * <p>Concurrent transformations on the same Transformer object are not allowed.
679    *
680    * <p>The output is an MP4 file. It can contain at most one video track and one audio track. Other
681    * track types are ignored. For adaptive bitrate {@linkplain MediaSource media sources}, the
682    * highest bitrate video and audio streams are selected.
683    *
684    * @param mediaItem The {@link MediaItem} to transform.
685    * @param parcelFileDescriptor A readable and writable {@link ParcelFileDescriptor} of the output.
686    *     The file referenced by this ParcelFileDescriptor should not be used before the
687    *     transformation is completed. It is the responsibility of the caller to close the
688    *     ParcelFileDescriptor. This can be done after this method returns.
689    * @throws IllegalArgumentException If the file descriptor is invalid.
690    * @throws IllegalStateException If this method is called from the wrong thread.
691    * @throws IllegalStateException If a transformation is already in progress.
692    * @throws IOException If an error occurs opening the output file for writing.
693    */
694   @RequiresApi(26)
startTransformation(MediaItem mediaItem, ParcelFileDescriptor parcelFileDescriptor)695   public void startTransformation(MediaItem mediaItem, ParcelFileDescriptor parcelFileDescriptor)
696       throws IOException {
697     startTransformation(mediaItem, muxerFactory.create(parcelFileDescriptor, containerMimeType));
698   }
699 
startTransformation(MediaItem mediaItem, Muxer muxer)700   private void startTransformation(MediaItem mediaItem, Muxer muxer) {
701     verifyApplicationThread();
702     if (player != null) {
703       throw new IllegalStateException("There is already a transformation in progress.");
704     }
705     MuxerWrapper muxerWrapper = new MuxerWrapper(muxer, muxerFactory, containerMimeType);
706     this.muxerWrapper = muxerWrapper;
707     DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
708     trackSelector.setParameters(
709         new DefaultTrackSelector.ParametersBuilder(context)
710             .setForceHighestSupportedBitrate(true)
711             .build());
712     // Arbitrarily decrease buffers for playback so that samples start being sent earlier to the
713     // muxer (rebuffers are less problematic for the transformation use case).
714     DefaultLoadControl loadControl =
715         new DefaultLoadControl.Builder()
716             .setBufferDurationsMs(
717                 DEFAULT_MIN_BUFFER_MS,
718                 DEFAULT_MAX_BUFFER_MS,
719                 DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10,
720                 DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10)
721             .build();
722     ExoPlayer.Builder playerBuilder =
723         new ExoPlayer.Builder(
724                 context,
725                 new TransformerRenderersFactory(
726                     context,
727                     muxerWrapper,
728                     removeAudio,
729                     removeVideo,
730                     transformationRequest,
731                     frameProcessors,
732                     encoderFactory,
733                     decoderFactory,
734                     new FallbackListener(mediaItem, listeners, transformationRequest),
735                     debugViewProvider))
736             .setMediaSourceFactory(mediaSourceFactory)
737             .setTrackSelector(trackSelector)
738             .setLoadControl(loadControl)
739             .setLooper(looper);
740     if (clock != Clock.DEFAULT) {
741       // Transformer.Builder#setClock is also @VisibleForTesting, so if we're using a non-default
742       // clock we must be in a test context.
743       @SuppressWarnings("VisibleForTests")
744       ExoPlayer.Builder unusedForAnnotation = playerBuilder.setClock(clock);
745     }
746 
747     player = playerBuilder.build();
748     player.setMediaItem(mediaItem);
749     player.addListener(new TransformerPlayerListener(mediaItem, muxerWrapper));
750     player.prepare();
751 
752     progressState = PROGRESS_STATE_WAITING_FOR_AVAILABILITY;
753   }
754 
755   /**
756    * Returns the {@link Looper} associated with the application thread that's used to access the
757    * transformer and on which transformer events are received.
758    */
getApplicationLooper()759   public Looper getApplicationLooper() {
760     return looper;
761   }
762 
763   /**
764    * Returns the current {@link ProgressState} and updates {@code progressHolder} with the current
765    * progress if it is {@link #PROGRESS_STATE_AVAILABLE available}.
766    *
767    * <p>After a transformation {@linkplain Listener#onTransformationCompleted(MediaItem,
768    * TransformationResult) completes}, this method returns {@link
769    * #PROGRESS_STATE_NO_TRANSFORMATION}.
770    *
771    * @param progressHolder A {@link ProgressHolder}, updated to hold the percentage progress if
772    *     {@link #PROGRESS_STATE_AVAILABLE available}.
773    * @return The {@link ProgressState}.
774    * @throws IllegalStateException If this method is called from the wrong thread.
775    */
getProgress(ProgressHolder progressHolder)776   public @ProgressState int getProgress(ProgressHolder progressHolder) {
777     verifyApplicationThread();
778     if (progressState == PROGRESS_STATE_AVAILABLE) {
779       Player player = checkNotNull(this.player);
780       long durationMs = player.getDuration();
781       long positionMs = player.getCurrentPosition();
782       progressHolder.progress = min((int) (positionMs * 100 / durationMs), 99);
783     }
784     return progressState;
785   }
786 
787   /**
788    * Cancels the transformation that is currently in progress, if any.
789    *
790    * @throws IllegalStateException If this method is called from the wrong thread.
791    */
cancel()792   public void cancel() {
793     isCancelling = true;
794     try {
795       releaseResources(/* forCancellation= */ true);
796     } catch (TransformationException impossible) {
797       throw new IllegalStateException(impossible);
798     }
799     isCancelling = false;
800   }
801 
802   /**
803    * Releases the resources.
804    *
805    * @param forCancellation Whether the reason for releasing the resources is the transformation
806    *     cancellation.
807    * @throws IllegalStateException If this method is called from the wrong thread.
808    * @throws TransformationException If the muxer is in the wrong state and {@code forCancellation}
809    *     is false.
810    */
releaseResources(boolean forCancellation)811   private void releaseResources(boolean forCancellation) throws TransformationException {
812     verifyApplicationThread();
813     progressState = PROGRESS_STATE_NO_TRANSFORMATION;
814     if (player != null) {
815       player.release();
816       player = null;
817     }
818     if (muxerWrapper != null) {
819       try {
820         muxerWrapper.release(forCancellation);
821       } catch (Muxer.MuxerException e) {
822         throw TransformationException.createForMuxer(
823             e, TransformationException.ERROR_CODE_MUXING_FAILED);
824       }
825       muxerWrapper = null;
826     }
827   }
828 
verifyApplicationThread()829   private void verifyApplicationThread() {
830     if (Looper.myLooper() != looper) {
831       throw new IllegalStateException("Transformer is accessed on the wrong thread.");
832     }
833   }
834 
835   private static final class TransformerRenderersFactory implements RenderersFactory {
836 
837     private final Context context;
838     private final MuxerWrapper muxerWrapper;
839     private final TransformerMediaClock mediaClock;
840     private final boolean removeAudio;
841     private final boolean removeVideo;
842     private final TransformationRequest transformationRequest;
843     private final ImmutableList<GlFrameProcessor> frameProcessors;
844     private final Codec.EncoderFactory encoderFactory;
845     private final Codec.DecoderFactory decoderFactory;
846     private final FallbackListener fallbackListener;
847     private final Transformer.DebugViewProvider debugViewProvider;
848 
TransformerRenderersFactory( Context context, MuxerWrapper muxerWrapper, boolean removeAudio, boolean removeVideo, TransformationRequest transformationRequest, ImmutableList<GlFrameProcessor> frameProcessors, Codec.EncoderFactory encoderFactory, Codec.DecoderFactory decoderFactory, FallbackListener fallbackListener, Transformer.DebugViewProvider debugViewProvider)849     public TransformerRenderersFactory(
850         Context context,
851         MuxerWrapper muxerWrapper,
852         boolean removeAudio,
853         boolean removeVideo,
854         TransformationRequest transformationRequest,
855         ImmutableList<GlFrameProcessor> frameProcessors,
856         Codec.EncoderFactory encoderFactory,
857         Codec.DecoderFactory decoderFactory,
858         FallbackListener fallbackListener,
859         Transformer.DebugViewProvider debugViewProvider) {
860       this.context = context;
861       this.muxerWrapper = muxerWrapper;
862       this.removeAudio = removeAudio;
863       this.removeVideo = removeVideo;
864       this.transformationRequest = transformationRequest;
865       this.frameProcessors = frameProcessors;
866       this.encoderFactory = encoderFactory;
867       this.decoderFactory = decoderFactory;
868       this.fallbackListener = fallbackListener;
869       this.debugViewProvider = debugViewProvider;
870       mediaClock = new TransformerMediaClock();
871     }
872 
873     @Override
createRenderers( Handler eventHandler, VideoRendererEventListener videoRendererEventListener, AudioRendererEventListener audioRendererEventListener, TextOutput textRendererOutput, MetadataOutput metadataRendererOutput)874     public Renderer[] createRenderers(
875         Handler eventHandler,
876         VideoRendererEventListener videoRendererEventListener,
877         AudioRendererEventListener audioRendererEventListener,
878         TextOutput textRendererOutput,
879         MetadataOutput metadataRendererOutput) {
880       int rendererCount = removeAudio || removeVideo ? 1 : 2;
881       Renderer[] renderers = new Renderer[rendererCount];
882       int index = 0;
883       if (!removeAudio) {
884         renderers[index] =
885             new TransformerAudioRenderer(
886                 muxerWrapper,
887                 mediaClock,
888                 transformationRequest,
889                 encoderFactory,
890                 decoderFactory,
891                 fallbackListener);
892         index++;
893       }
894       if (!removeVideo) {
895         renderers[index] =
896             new TransformerVideoRenderer(
897                 context,
898                 muxerWrapper,
899                 mediaClock,
900                 transformationRequest,
901                 frameProcessors,
902                 encoderFactory,
903                 decoderFactory,
904                 fallbackListener,
905                 debugViewProvider);
906         index++;
907       }
908       return renderers;
909     }
910   }
911 
912   private final class TransformerPlayerListener implements Player.Listener {
913 
914     private final MediaItem mediaItem;
915     private final MuxerWrapper muxerWrapper;
916 
TransformerPlayerListener(MediaItem mediaItem, MuxerWrapper muxerWrapper)917     public TransformerPlayerListener(MediaItem mediaItem, MuxerWrapper muxerWrapper) {
918       this.mediaItem = mediaItem;
919       this.muxerWrapper = muxerWrapper;
920     }
921 
922     @Override
onPlaybackStateChanged(int state)923     public void onPlaybackStateChanged(int state) {
924       if (state == Player.STATE_ENDED) {
925         handleTransformationEnded(/* exception= */ null);
926       }
927     }
928 
929     @Override
onTimelineChanged(Timeline timeline, int reason)930     public void onTimelineChanged(Timeline timeline, int reason) {
931       if (progressState != PROGRESS_STATE_WAITING_FOR_AVAILABILITY) {
932         return;
933       }
934       Timeline.Window window = new Timeline.Window();
935       timeline.getWindow(/* windowIndex= */ 0, window);
936       if (!window.isPlaceholder) {
937         long durationUs = window.durationUs;
938         // Make progress permanently unavailable if the duration is unknown, so that it doesn't jump
939         // to a high value at the end of the transformation if the duration is set once the media is
940         // entirely loaded.
941         progressState =
942             durationUs <= 0 || durationUs == C.TIME_UNSET
943                 ? PROGRESS_STATE_UNAVAILABLE
944                 : PROGRESS_STATE_AVAILABLE;
945         checkNotNull(player).play();
946       }
947     }
948 
949     @Override
onTracksInfoChanged(TracksInfo tracksInfo)950     public void onTracksInfoChanged(TracksInfo tracksInfo) {
951       if (muxerWrapper.getTrackCount() == 0) {
952         handleTransformationEnded(
953             TransformationException.createForUnexpected(
954                 new IllegalStateException("The output does not contain any tracks.")));
955       }
956     }
957 
958     @Override
onPlayerError(PlaybackException error)959     public void onPlayerError(PlaybackException error) {
960       @Nullable Throwable cause = error.getCause();
961       TransformationException transformationException =
962           cause instanceof TransformationException
963               ? (TransformationException) cause
964               : TransformationException.createForPlaybackException(error);
965       if (isCancelling) {
966         // Resources are already being released.
967         listeners.queueEvent(
968             /* eventFlag= */ C.INDEX_UNSET,
969             listener -> listener.onTransformationError(mediaItem, transformationException));
970         listeners.flushEvents();
971       } else {
972         handleTransformationEnded(transformationException);
973       }
974     }
975 
handleTransformationEnded(@ullable TransformationException exception)976     private void handleTransformationEnded(@Nullable TransformationException exception) {
977       @Nullable TransformationException resourceReleaseException = null;
978       try {
979         releaseResources(/* forCancellation= */ false);
980       } catch (TransformationException e) {
981         resourceReleaseException = e;
982       } catch (RuntimeException e) {
983         resourceReleaseException = TransformationException.createForUnexpected(e);
984       }
985       if (exception == null) {
986         // We only report the exception caused by releasing the resources if there is no other
987         // exception. It is more intuitive to call the error callback only once and reporting the
988         // exception caused by releasing the resources can be confusing if it is a consequence of
989         // the first exception.
990         exception = resourceReleaseException;
991       }
992 
993       if (exception != null) {
994         TransformationException finalException = exception;
995         // TODO(b/213341814): Add event flags for Transformer events.
996         listeners.queueEvent(
997             /* eventFlag= */ C.INDEX_UNSET,
998             listener -> listener.onTransformationError(mediaItem, finalException));
999       } else {
1000         TransformationResult result =
1001             new TransformationResult.Builder()
1002                 .setDurationMs(muxerWrapper.getDurationMs())
1003                 .setAverageAudioBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_AUDIO))
1004                 .setAverageVideoBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_VIDEO))
1005                 .setVideoFrameCount(muxerWrapper.getTrackSampleCount(C.TRACK_TYPE_VIDEO))
1006                 .build();
1007 
1008         listeners.queueEvent(
1009             /* eventFlag= */ C.INDEX_UNSET,
1010             listener -> listener.onTransformationCompleted(mediaItem, result));
1011       }
1012       listeners.flushEvents();
1013     }
1014   }
1015 }
1016