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