java.lang.Object
↳androidx.media3.transformer.Transformer
Gradle dependencies
compile group: 'androidx.media3', name: 'media3-transformer', version: '1.5.0-alpha01'
- groupId: androidx.media3
- artifactId: media3-transformer
- version: 1.5.0-alpha01
Artifact androidx.media3:media3-transformer:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)
Overview
A transformer to export media inputs.
The same Transformer instance can be used to export multiple inputs (sequentially, not
concurrently).
Transformer instances must be accessed from a single application thread. For the vast majority
of cases this should be the application's main thread. The thread on which a Transformer instance
must be accessed can be explicitly specified by passing a when creating the
transformer. If no Looper is specified, then the Looper of the thread that the Transformer.Builder is created on is used, or if that thread does not have a Looper, the Looper
of the application's main thread is used. In all cases the Looper of the thread from which the
transformer must be accessed can be queried using Transformer.getApplicationLooper().
Summary
Methods |
---|
public void | addListener(Transformer.Listener listener)
Adds a Transformer.Listener to listen to the export events. |
public Transformer.Builder | buildUpon()
Returns a Transformer.Builder initialized with the values of this instance. |
public void | cancel()
Cancels the export that is currently in progress, if any. |
public Looper | getApplicationLooper()
Returns the associated with the application thread that's used to access the
transformer and on which transformer events are received. |
public int | getProgress(ProgressHolder progressHolder)
Returns the current Transformer.ProgressState and updates progressHolder with the current
progress if it is available. |
public void | removeAllListeners()
Removes all listeners. |
public void | removeListener(Transformer.Listener listener)
Removes a Transformer.Listener. |
public void | resume(Composition composition, java.lang.String outputFilePath, java.lang.String oldFilePath)
Resumes a previously cancelled export. |
public void | setListener(Transformer.Listener listener)
|
public void | start(Composition composition, java.lang.String path)
Starts an asynchronous operation to export the given Composition. |
public void | start(EditedMediaItem editedMediaItem, java.lang.String path)
Starts an asynchronous operation to export the given EditedMediaItem. |
public void | start(MediaItem mediaItem, java.lang.String path)
Starts an asynchronous operation to export the given MediaItem. |
public void | startTransformation(MediaItem mediaItem, java.lang.String path)
|
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Fields
public static final int
PROGRESS_STATE_NOT_STARTEDIndicates that the corresponding operation hasn't been started.
public static final int
PROGRESS_STATE_NO_TRANSFORMATIONDeprecated: Use Transformer.PROGRESS_STATE_NOT_STARTED instead.
public static final int
PROGRESS_STATE_WAITING_FOR_AVAILABILITYIndicates that the progress is currently unavailable, but might become available.
public static final int
PROGRESS_STATE_AVAILABLEIndicates that the progress is available.
public static final int
PROGRESS_STATE_UNAVAILABLEIndicates that the progress is permanently unavailable.
public static final long
DEFAULT_MAX_DELAY_BETWEEN_MUXER_SAMPLES_MSThe default value for the maximum delay
between output samples.
Methods
Returns a Transformer.Builder initialized with the values of this instance.
Deprecated: Use Transformer.addListener(Transformer.Listener), Transformer.removeListener(Transformer.Listener) or Transformer.removeAllListeners() instead.
Adds a Transformer.Listener to listen to the export events.
Parameters:
listener: A Transformer.Listener.
Removes a Transformer.Listener.
Parameters:
listener: A Transformer.Listener.
public void
removeAllListeners()
Removes all listeners.
public void
start(
Composition composition, java.lang.String path)
Starts an asynchronous operation to export the given Composition.
The first EditedMediaItem in the first EditedMediaItemSequence that has a
given will determine the output format for that track, unless
the format is set when building the Transformer. For
example, consider the following composition
Composition {
EditedMediaItemSequence {
[ImageMediaItem, VideoMediaItem]
},
EditedMediaItemSequence {
[AudioMediaItem]
},
}
The video format will be determined by the ImageMediaItem in the first
EditedMediaItemSequence, while the audio format will be determined by the AudioMediaItem in the second EditedMediaItemSequence.
This method is under development. A Composition must meet the following conditions:
- The video composition Presentation effect is applied after input streams are
composited. Other composition effects are ignored.
Sequences within the Composition must meet the
following conditions:
The export state is notified through the listener.
Concurrent exports on the same Transformer object are not allowed.
If no custom Muxer.Factory is
specified, the output is an MP4 file.
The output can contain at most one video track and one audio track. Other track types are
ignored. For adaptive bitrate inputs, if no custom AssetLoader.Factory is
specified, the highest bitrate video and audio streams are selected.
If exporting the video track entails transcoding, the output frames' dimensions will be
swapped if the output video's height is larger than the width. This is to improve compatibility
among different device encoders.
Parameters:
composition: The Composition to export.
path: The path to the output file.
Starts an asynchronous operation to export the given EditedMediaItem.
The export state is notified through the listener.
Concurrent exports on the same Transformer object are not allowed.
If no custom Muxer.Factory is
specified, the output is an MP4 file.
The output can contain at most one video track and one audio track. Other track types are
ignored. For adaptive bitrate inputs, if no custom AssetLoader.Factory is
specified, the highest bitrate video and audio streams are selected.
If exporting the video track entails transcoding, the output frames' dimensions will be
swapped if the output video's height is larger than the width. This is to improve compatibility
among different device encoders.
Parameters:
editedMediaItem: The EditedMediaItem to export.
path: The path to the output file.
public void
start(
MediaItem mediaItem, java.lang.String path)
Starts an asynchronous operation to export the given MediaItem.
The export state is notified through the listener.
Concurrent exports on the same Transformer object are not allowed.
If no custom Muxer.Factory is
specified, the output is an MP4 file.
The output can contain at most one video track and one audio track. Other track types are
ignored. For adaptive bitrate inputs, if no custom AssetLoader.Factory is
specified, the highest bitrate video and audio streams are selected.
If exporting the video track entails transcoding, the output frames' dimensions will be
swapped if the output video's height is larger than the width. This is to improve compatibility
among different device encoders.
Parameters:
mediaItem: The MediaItem to export.
path: The path to the output file.
public void
startTransformation(
MediaItem mediaItem, java.lang.String path)
Deprecated: Use Transformer.start(MediaItem, String) instead.
public Looper
getApplicationLooper()
Returns the associated with the application thread that's used to access the
transformer and on which transformer events are received.
Returns the current Transformer.ProgressState and updates progressHolder with the current
progress if it is available.
If the export is resumed, this method
returns Transformer.PROGRESS_STATE_UNAVAILABLE.
After an export completes, this
method returns Transformer.PROGRESS_STATE_NOT_STARTED.
Parameters:
progressHolder: A ProgressHolder, updated to hold the percentage progress if
available.
Returns:
The Transformer.ProgressState.
Cancels the export that is currently in progress, if any.
The export output file (if any) is not deleted.
public void
resume(
Composition composition, java.lang.String outputFilePath, java.lang.String oldFilePath)
Resumes a previously cancelled export.
An export can be resumed only when:
Note that export optimizations (such as trim optimization) will not be applied upon
resumption.
Parameters:
composition: The Composition to resume export.
outputFilePath: The path to the output file. This must be different from the output path
of the cancelled export.
oldFilePath: The output path of the the cancelled export.
Source
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.isRunningOnEmulator;
import static androidx.media3.extractor.AacUtil.AAC_LC_AUDIO_SAMPLE_COUNT;
import static androidx.media3.transformer.ExportException.ERROR_CODE_MUXING_APPEND;
import static androidx.media3.transformer.ExportResult.OPTIMIZATION_ABANDONED_KEYFRAME_PLACEMENT_OPTIMAL_FOR_TRIM;
import static androidx.media3.transformer.ExportResult.OPTIMIZATION_ABANDONED_OTHER;
import static androidx.media3.transformer.ExportResult.OPTIMIZATION_ABANDONED_TRIM_AND_TRANSCODING_TRANSFORMATION_REQUESTED;
import static androidx.media3.transformer.ExportResult.OPTIMIZATION_FAILED_EXTRACTION_FAILED;
import static androidx.media3.transformer.ExportResult.OPTIMIZATION_FAILED_FORMAT_MISMATCH;
import static androidx.media3.transformer.TransformerUtil.maybeSetMuxerWrapperAdditionalRotationDegrees;
import static androidx.media3.transformer.TransformerUtil.shouldTranscodeAudio;
import static androidx.media3.transformer.TransformerUtil.shouldTranscodeVideo;
import static androidx.media3.transformer.TransmuxTranscodeHelper.buildUponCompositionForTrimOptimization;
import static java.lang.Math.round;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context;
import android.os.Looper;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.DebugViewProvider;
import androidx.media3.common.Effect;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.audio.AudioProcessor;
import androidx.media3.common.audio.AudioProcessor.AudioFormat;
import androidx.media3.common.audio.ChannelMixingAudioProcessor;
import androidx.media3.common.audio.SonicAudioProcessor;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.effect.DebugTraceUtil;
import androidx.media3.effect.DefaultVideoFrameProcessor;
import androidx.media3.effect.Presentation;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.muxer.Muxer;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.InlineMe;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A transformer to export media inputs.
*
* <p>The same Transformer instance can be used to export multiple inputs (sequentially, not
* concurrently).
*
* <p>Transformer instances must be accessed from a single application thread. For the vast majority
* of cases this should be the application's main thread. The thread on which a Transformer instance
* must be accessed can be explicitly specified by passing a {@link Looper} when creating the
* transformer. If no Looper is specified, then the Looper of the thread that the {@link
* Transformer.Builder} is created on is used, or if that thread does not have a Looper, the Looper
* of the application's main thread is used. In all cases the Looper of the thread from which the
* transformer must be accessed can be queried using {@link #getApplicationLooper()}.
*/
@UnstableApi
public final class Transformer {
static {
MediaLibraryInfo.registerModule("media3.transformer");
}
/** A builder for {@link Transformer} instances. */
public static final class Builder {
// Mandatory field.
private final Context context;
// Optional fields.
private @MonotonicNonNull String audioMimeType;
private @MonotonicNonNull String videoMimeType;
private @MonotonicNonNull TransformationRequest transformationRequest;
private ImmutableList<AudioProcessor> audioProcessors;
private ImmutableList<Effect> videoEffects;
private boolean removeAudio;
private boolean removeVideo;
private boolean flattenForSlowMotion;
private boolean trimOptimizationEnabled;
private boolean fileStartsOnVideoFrameEnabled;
private long maxDelayBetweenMuxerSamplesMs;
private int maxFramesInEncoder;
private ListenerSet<Transformer.Listener> listeners;
private AssetLoader.@MonotonicNonNull Factory assetLoaderFactory;
private AudioMixer.Factory audioMixerFactory;
private VideoFrameProcessor.Factory videoFrameProcessorFactory;
private Codec.EncoderFactory encoderFactory;
private Muxer.Factory muxerFactory;
private Looper looper;
private DebugViewProvider debugViewProvider;
private Clock clock;
/**
* Creates a builder with default values.
*
* @param context The {@link Context}.
*/
public Builder(Context context) {
this.context = context.getApplicationContext();
maxDelayBetweenMuxerSamplesMs = DEFAULT_MAX_DELAY_BETWEEN_MUXER_SAMPLES_MS;
maxFramesInEncoder = C.INDEX_UNSET;
audioProcessors = ImmutableList.of();
videoEffects = ImmutableList.of();
audioMixerFactory = new DefaultAudioMixer.Factory();
videoFrameProcessorFactory = new DefaultVideoFrameProcessor.Factory.Builder().build();
encoderFactory = new DefaultEncoderFactory.Builder(this.context).build();
muxerFactory = new DefaultMuxer.Factory();
looper = Util.getCurrentOrMainLooper();
debugViewProvider = DebugViewProvider.NONE;
clock = Clock.DEFAULT;
listeners = new ListenerSet<>(looper, clock, (listener, flags) -> {});
}
/** Creates a builder with the values of the provided {@link Transformer}. */
private Builder(Transformer transformer) {
this.context = transformer.context;
this.audioMimeType = transformer.transformationRequest.audioMimeType;
this.videoMimeType = transformer.transformationRequest.videoMimeType;
this.transformationRequest = transformer.transformationRequest;
this.audioProcessors = transformer.audioProcessors;
this.videoEffects = transformer.videoEffects;
this.removeAudio = transformer.removeAudio;
this.removeVideo = transformer.removeVideo;
this.trimOptimizationEnabled = transformer.trimOptimizationEnabled;
this.fileStartsOnVideoFrameEnabled = transformer.fileStartsOnVideoFrameEnabled;
this.maxDelayBetweenMuxerSamplesMs = transformer.maxDelayBetweenMuxerSamplesMs;
this.maxFramesInEncoder = transformer.maxFramesInEncoder;
this.listeners = transformer.listeners;
this.assetLoaderFactory = transformer.assetLoaderFactory;
this.audioMixerFactory = transformer.audioMixerFactory;
this.videoFrameProcessorFactory = transformer.videoFrameProcessorFactory;
this.encoderFactory = transformer.encoderFactory;
this.muxerFactory = transformer.muxerFactory;
this.looper = transformer.looper;
this.debugViewProvider = transformer.debugViewProvider;
this.clock = transformer.clock;
}
/**
* Sets the audio {@linkplain MimeTypes MIME type} of the output.
*
* <p>If no audio MIME type is passed, the output audio MIME type is the same as the first
* {@link MediaItem} in the {@link Composition}.
*
* <p>Supported MIME types are:
*
* <ul>
* <li>{@link MimeTypes#AUDIO_AAC}
* <li>{@link MimeTypes#AUDIO_AMR_NB}
* <li>{@link MimeTypes#AUDIO_AMR_WB}
* </ul>
*
* If the MIME type is not supported, {@link Transformer} will fallback to a supported MIME type
* and {@link Listener#onFallbackApplied(Composition, TransformationRequest,
* TransformationRequest)} will be invoked with the fallback value.
*
* @param audioMimeType The MIME type of the audio samples in the output.
* @return This builder.
* @throws IllegalArgumentException If the audio MIME type passed is not an audio {@linkplain
* MimeTypes MIME type}.
*/
@CanIgnoreReturnValue
public Builder setAudioMimeType(String audioMimeType) {
audioMimeType = MimeTypes.normalizeMimeType(audioMimeType);
checkArgument(MimeTypes.isAudio(audioMimeType), "Not an audio MIME type: " + audioMimeType);
this.audioMimeType = audioMimeType;
return this;
}
/**
* Sets the video {@linkplain MimeTypes MIME type} of the output.
*
* <p>If no video MIME type is passed, the output video MIME type is the same as the first
* {@link MediaItem} in the {@link Composition}.
*
* <p>Supported MIME types are:
*
* <ul>
* <li>{@link MimeTypes#VIDEO_H263}
* <li>{@link MimeTypes#VIDEO_H264}
* <li>{@link MimeTypes#VIDEO_H265} from API level 24
* <li>{@link MimeTypes#VIDEO_MP4V}
* </ul>
*
* If the MIME type is not supported, {@link Transformer} will fallback to a supported MIME type
* and {@link Listener#onFallbackApplied(Composition, TransformationRequest,
* TransformationRequest)} will be invoked with the fallback value.
*
* @param videoMimeType The MIME type of the video samples in the output.
* @return This builder.
* @throws IllegalArgumentException If the video MIME type passed is not a video {@linkplain
* MimeTypes MIME type}.
*/
@CanIgnoreReturnValue
public Builder setVideoMimeType(String videoMimeType) {
videoMimeType = MimeTypes.normalizeMimeType(videoMimeType);
checkArgument(MimeTypes.isVideo(videoMimeType), "Not a video MIME type: " + videoMimeType);
this.videoMimeType = videoMimeType;
return this;
}
/**
* @deprecated Use {@link #setAudioMimeType(String)}, {@link #setVideoMimeType(String)} and
* {@link Composition.Builder#setHdrMode(int)} instead.
*/
@Deprecated
@CanIgnoreReturnValue
public Builder setTransformationRequest(TransformationRequest transformationRequest) {
// TODO(b/289872787): Make TransformationRequest.Builder package private once this method is
// removed.
this.transformationRequest = transformationRequest;
return this;
}
/**
* @deprecated Set the {@linkplain AudioProcessor audio processors} in an {@link
* EditedMediaItem.Builder#setEffects(Effects)}, and pass it to {@link
* #start(EditedMediaItem, String)} instead.
*/
@CanIgnoreReturnValue
@Deprecated
public Builder setAudioProcessors(List<AudioProcessor> audioProcessors) {
this.audioProcessors = ImmutableList.copyOf(audioProcessors);
return this;
}
/**
* @deprecated Set the {@linkplain Effect video effects} in an {@link
* EditedMediaItem.Builder#setEffects(Effects)}, and pass it to {@link
* #start(EditedMediaItem, String)} instead.
*/
@CanIgnoreReturnValue
@Deprecated
public Builder setVideoEffects(List<Effect> effects) {
this.videoEffects = ImmutableList.copyOf(effects);
return this;
}
/**
* @deprecated Use {@link EditedMediaItem.Builder#setRemoveAudio(boolean)} to remove the audio
* from the {@link EditedMediaItem} passed to {@link #start(EditedMediaItem, String)}
* instead.
*/
@CanIgnoreReturnValue
@Deprecated
public Builder setRemoveAudio(boolean removeAudio) {
this.removeAudio = removeAudio;
return this;
}
/**
* @deprecated Use {@link EditedMediaItem.Builder#setRemoveVideo(boolean)} to remove the video
* from the {@link EditedMediaItem} passed to {@link #start(EditedMediaItem, String)}
* instead.
*/
@CanIgnoreReturnValue
@Deprecated
public Builder setRemoveVideo(boolean removeVideo) {
this.removeVideo = removeVideo;
return this;
}
/**
* @deprecated Use {@link EditedMediaItem.Builder#setFlattenForSlowMotion(boolean)} to flatten
* the {@link EditedMediaItem} passed to {@link #start(EditedMediaItem, String)} instead.
*/
@CanIgnoreReturnValue
@Deprecated
public Builder setFlattenForSlowMotion(boolean flattenForSlowMotion) {
this.flattenForSlowMotion = flattenForSlowMotion;
return this;
}
/**
* Sets whether to attempt to optimize trims from the start of the {@link EditedMediaItem} by
* transcoding as little of the file as possible and transmuxing the rest.
*
* <p>This optimization has the following limitations:
*
* <ul>
* <li>Only supported for single-asset (i.e. only one {@link EditedMediaItem} in the whole
* {@link Composition}) exports of mp4 files.
* <li>Not guaranteed to work with any effects.
* </ul>
*
* <p>This process relies on the given {@linkplain #setEncoderFactory EncoderFactory} providing
* the right encoder level and profiles when transcoding, so that the transcoded and transmuxed
* segments of the file can be stitched together. If the file segments can't be stitched
* together, Transformer throw away any progress and proceed with unoptimized export instead.
*
* <p>The {@link ExportResult#optimizationResult} will indicate whether the optimization was
* applied.
*
* @param enabled Whether to enable trim optimization.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder experimentalSetTrimOptimizationEnabled(boolean enabled) {
trimOptimizationEnabled = enabled;
return this;
}
/**
* Limits how many video frames can be processed at any time by the {@linkplain Codec encoder}.
*
* <p>A video frame starts encoding when it enters the {@linkplain Codec#getInputSurface()
* encoder input surface}, and finishes encoding when the corresponding {@linkplain
* Codec#releaseOutputBuffer encoder output buffer is released}.
*
* <p>The default value is {@link C#INDEX_UNSET}, which means no limit is enforced.
*
* <p>This method is experimental and will be renamed or removed in a future release.
*
* @param maxFramesInEncoder The maximum number of frames that the video encoder is allowed to
* process at a time, or {@link C#INDEX_UNSET} if no limit is enforced.
* @return This builder.
* @throws IllegalArgumentException If {@code maxFramesInEncoder} is not equal to {@link
* C#INDEX_UNSET} and is non-positive.
*/
@CanIgnoreReturnValue
public Builder experimentalSetMaxFramesInEncoder(int maxFramesInEncoder) {
checkArgument(maxFramesInEncoder > 0 || maxFramesInEncoder == C.INDEX_UNSET);
this.maxFramesInEncoder = maxFramesInEncoder;
return this;
}
/**
* Sets whether to ensure that the output file starts on a video frame.
*
* <p>Any audio samples that are earlier than the first video frame will be dropped. This can
* make the output of trimming operations more compatible with player implementations that don't
* show the first video frame until its presentation timestamp.
*
* <p>Ignored when {@linkplain #experimentalSetTrimOptimizationEnabled trim optimization} is
* set.
*
* @param enabled Whether to ensure that the file starts on a video frame.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setEnsureFileStartsOnVideoFrameEnabled(boolean enabled) {
fileStartsOnVideoFrameEnabled = enabled;
return this;
}
/**
* Sets the maximum delay allowed between output samples regardless of the track type, or {@link
* C#TIME_UNSET} if there is no maximum. The default value is {@link
* #DEFAULT_MAX_DELAY_BETWEEN_MUXER_SAMPLES_MS}.
*
* <p>The export will be aborted when no sample is written in {@code
* maxDelayBetweenMuxerSamplesMs}. Note that there is no guarantee that the export will be
* aborted exactly at that time.
*
* @param maxDelayBetweenMuxerSamplesMs The maximum delay allowed (in microseconds).
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMaxDelayBetweenMuxerSamplesMs(long maxDelayBetweenMuxerSamplesMs) {
this.maxDelayBetweenMuxerSamplesMs = maxDelayBetweenMuxerSamplesMs;
return this;
}
/**
* @deprecated Use {@link #addListener(Listener)}, {@link #removeListener(Listener)} or {@link
* #removeAllListeners()} instead.
*/
@CanIgnoreReturnValue
@Deprecated
public Builder setListener(Transformer.Listener listener) {
this.listeners.clear();
this.listeners.add(listener);
return this;
}
/**
* Adds a {@link Transformer.Listener} to listen to the export events.
*
* <p>This is equivalent to {@link Transformer#addListener(Listener)}.
*
* @param listener A {@link Transformer.Listener}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder addListener(Transformer.Listener listener) {
this.listeners.add(listener);
return this;
}
/**
* Removes a {@link Transformer.Listener}.
*
* <p>This is equivalent to {@link Transformer#removeListener(Listener)}.
*
* @param listener A {@link Transformer.Listener}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder removeListener(Transformer.Listener listener) {
this.listeners.remove(listener);
return this;
}
/**
* Removes all {@linkplain Transformer.Listener listeners}.
*
* <p>This is equivalent to {@link Transformer#removeAllListeners()}.
*
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder removeAllListeners() {
this.listeners.clear();
return this;
}
/**
* Sets the {@link AssetLoader.Factory} to be used to retrieve the samples to export.
*
* <p>The default value is a {@link DefaultAssetLoaderFactory} built with a {@link
* DefaultMediaSourceFactory} and a {@link DefaultDecoderFactory}.
*
* @param assetLoaderFactory An {@link AssetLoader.Factory}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setAssetLoaderFactory(AssetLoader.Factory assetLoaderFactory) {
this.assetLoaderFactory = assetLoaderFactory;
return this;
}
/**
* Sets the {@link AudioMixer.Factory} to be used when {@linkplain AudioMixer audio mixing} is
* needed.
*
* <p>The default value is a {@link DefaultAudioMixer.Factory} with default values.
*
* @param audioMixerFactory A {@link AudioMixer.Factory}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setAudioMixerFactory(AudioMixer.Factory audioMixerFactory) {
this.audioMixerFactory = audioMixerFactory;
return this;
}
/**
* Sets the {@link VideoFrameProcessor.Factory} to be used to create {@link VideoFrameProcessor}
* instances.
*
* <p>The default value is a {@link DefaultVideoFrameProcessor.Factory} built with default
* values.
*
* <p>If passing in a {@link DefaultVideoFrameProcessor.Factory}, the caller must not {@link
* DefaultVideoFrameProcessor.Factory.Builder#setTextureOutput set the texture output}.
*
* <p>If exporting a {@link Composition} with multiple video {@linkplain EditedMediaItemSequence
* sequences}, the {@link VideoFrameProcessor.Factory} must be a {@link
* DefaultVideoFrameProcessor.Factory}.
*
* @param videoFrameProcessorFactory A {@link VideoFrameProcessor.Factory}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setVideoFrameProcessorFactory(
VideoFrameProcessor.Factory videoFrameProcessorFactory) {
this.videoFrameProcessorFactory = videoFrameProcessorFactory;
return this;
}
/**
* Sets the {@link Codec.EncoderFactory} that will be used by the transformer.
*
* <p>The default value is a {@link DefaultEncoderFactory} instance.
*
* @param encoderFactory The {@link Codec.EncoderFactory} instance.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setEncoderFactory(Codec.EncoderFactory encoderFactory) {
this.encoderFactory = encoderFactory;
return this;
}
/**
* Sets the {@link Muxer.Factory} for muxers that write the media container.
*
* <p>The default value is a {@link DefaultMuxer.Factory}.
*
* @param muxerFactory A {@link Muxer.Factory}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMuxerFactory(Muxer.Factory muxerFactory) {
this.muxerFactory = muxerFactory;
return this;
}
/**
* Sets the {@link Looper} that must be used for all calls to the transformer and that is used
* to call listeners on.
*
* <p>The default value is the Looper of the thread that this builder was created on, or if that
* thread does not have a Looper, the Looper of the application's main thread.
*
* @param looper A {@link Looper}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setLooper(Looper looper) {
this.looper = looper;
this.listeners = listeners.copy(looper, (listener, flags) -> {});
return this;
}
/**
* Sets a provider for views to show diagnostic information (if available) during export.
*
* <p>This is intended for debugging. The default value is {@link DebugViewProvider#NONE}, which
* doesn't show any debug info.
*
* <p>Not all exports will result in debug views being populated.
*
* @param debugViewProvider Provider for debug views.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setDebugViewProvider(DebugViewProvider debugViewProvider) {
this.debugViewProvider = debugViewProvider;
return this;
}
/**
* Sets the {@link Clock} that will be used by the transformer.
*
* <p>The default value is {@link Clock#DEFAULT}.
*
* @param clock The {@link Clock} instance.
* @return This builder.
*/
@CanIgnoreReturnValue
@VisibleForTesting
/* package */ Builder setClock(Clock clock) {
this.clock = clock;
this.listeners = listeners.copy(looper, clock, (listener, flags) -> {});
return this;
}
/**
* Builds a {@link Transformer} instance.
*
* @throws IllegalStateException If both audio and video have been removed (otherwise the output
* would not contain any samples).
* @throws IllegalStateException If the muxer doesn't support the requested audio/video MIME
* type.
*/
public Transformer build() {
TransformationRequest.Builder transformationRequestBuilder =
transformationRequest == null
? new TransformationRequest.Builder()
: transformationRequest.buildUpon();
if (audioMimeType != null) {
transformationRequestBuilder.setAudioMimeType(audioMimeType);
}
if (videoMimeType != null) {
transformationRequestBuilder.setVideoMimeType(videoMimeType);
}
transformationRequest = transformationRequestBuilder.build();
if (transformationRequest.audioMimeType != null) {
checkSampleMimeType(transformationRequest.audioMimeType);
}
if (transformationRequest.videoMimeType != null) {
checkSampleMimeType(transformationRequest.videoMimeType);
}
return new Transformer(
context,
transformationRequest,
audioProcessors,
videoEffects,
removeAudio,
removeVideo,
flattenForSlowMotion,
trimOptimizationEnabled,
fileStartsOnVideoFrameEnabled,
maxDelayBetweenMuxerSamplesMs,
maxFramesInEncoder,
listeners,
assetLoaderFactory,
audioMixerFactory,
videoFrameProcessorFactory,
encoderFactory,
muxerFactory,
looper,
debugViewProvider,
clock);
}
private void checkSampleMimeType(String sampleMimeType) {
checkState(
muxerFactory
.getSupportedSampleMimeTypes(MimeTypes.getTrackType(sampleMimeType))
.contains(sampleMimeType),
"Unsupported sample MIME type " + sampleMimeType);
}
}
/**
* A listener for the export events.
*
* <p>If the export is not cancelled, either {@link #onError} or {@link #onCompleted} will be
* called once for each export.
*/
public interface Listener {
/**
* @deprecated Use {@link #onCompleted(Composition, ExportResult)} instead.
*/
@Deprecated
default void onTransformationCompleted(MediaItem inputMediaItem) {}
/**
* @deprecated Use {@link #onCompleted(Composition, ExportResult)} instead.
*/
@SuppressWarnings("deprecation") // Using deprecated type in callback
@Deprecated
default void onTransformationCompleted(MediaItem inputMediaItem, TransformationResult result) {
onTransformationCompleted(inputMediaItem);
}
/**
* Called when the export is completed successfully.
*
* @param composition The {@link Composition} for which the export is completed.
* @param exportResult The {@link ExportResult} of the export.
*/
@SuppressWarnings("deprecation") // Calling deprecated listener method.
default void onCompleted(Composition composition, ExportResult exportResult) {
MediaItem mediaItem = composition.sequences.get(0).editedMediaItems.get(0).mediaItem;
onTransformationCompleted(mediaItem, new TransformationResult.Builder(exportResult).build());
}
/**
* @deprecated Use {@link #onError(Composition, ExportResult, ExportException)} instead.
*/
@Deprecated
default void onTransformationError(MediaItem inputMediaItem, Exception exception) {}
/**
* @deprecated Use {@link #onError(Composition, ExportResult, ExportException)} instead.
*/
@SuppressWarnings("deprecation") // Using deprecated type in callback
@Deprecated
default void onTransformationError(
MediaItem inputMediaItem, TransformationException exception) {
onTransformationError(inputMediaItem, (Exception) exception);
}
/**
* @deprecated Use {@link #onError(Composition, ExportResult, ExportException)} instead.
*/
@SuppressWarnings("deprecation") // Using deprecated type in callback
@Deprecated
default void onTransformationError(
MediaItem inputMediaItem, TransformationResult result, TransformationException exception) {
onTransformationError(inputMediaItem, exception);
}
/**
* Called if an exception occurs during the export.
*
* <p>The export output file (if any) is not deleted in this case.
*
* @param composition The {@link Composition} for which the exception occurs.
* @param exportResult The {@link ExportResult} of the export.
* @param exportException The {@link ExportException} describing the exception. This is the same
* instance as the {@linkplain ExportResult#exportException exception} in {@code result}.
*/
@SuppressWarnings("deprecation") // Calling deprecated listener method.
default void onError(
Composition composition, ExportResult exportResult, ExportException exportException) {
MediaItem mediaItem = composition.sequences.get(0).editedMediaItems.get(0).mediaItem;
onTransformationError(
mediaItem,
new TransformationResult.Builder(exportResult).build(),
new TransformationException(exportException));
}
/**
* @deprecated Use {@link #onFallbackApplied(Composition, TransformationRequest,
* TransformationRequest)} instead.
*/
@Deprecated
default void onFallbackApplied(
MediaItem inputMediaItem,
TransformationRequest originalTransformationRequest,
TransformationRequest fallbackTransformationRequest) {}
/**
* Called when falling back to an alternative {@link TransformationRequest} or changing the
* video frames' resolution is necessary to comply with muxer or device constraints.
*
* @param composition The {@link Composition} for which the export is requested.
* @param originalTransformationRequest The unsupported {@link TransformationRequest} used when
* building {@link Transformer}.
* @param fallbackTransformationRequest The alternative {@link TransformationRequest}, with
* supported {@link TransformationRequest#audioMimeType}, {@link
* TransformationRequest#videoMimeType}, {@link TransformationRequest#outputHeight}, and
* {@link TransformationRequest#hdrMode} values set.
*/
@SuppressWarnings("deprecation") // Calling deprecated listener method.
default void onFallbackApplied(
Composition composition,
TransformationRequest originalTransformationRequest,
TransformationRequest fallbackTransformationRequest) {
MediaItem mediaItem = composition.sequences.get(0).editedMediaItems.get(0).mediaItem;
onFallbackApplied(mediaItem, originalTransformationRequest, fallbackTransformationRequest);
}
}
/**
* Progress state. One of {@link #PROGRESS_STATE_NOT_STARTED}, {@link
* #PROGRESS_STATE_WAITING_FOR_AVAILABILITY}, {@link #PROGRESS_STATE_AVAILABLE} or {@link
* #PROGRESS_STATE_UNAVAILABLE}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
PROGRESS_STATE_NOT_STARTED,
PROGRESS_STATE_WAITING_FOR_AVAILABILITY,
PROGRESS_STATE_AVAILABLE,
PROGRESS_STATE_UNAVAILABLE
})
public @interface ProgressState {}
/** Indicates that the corresponding operation hasn't been started. */
public static final int PROGRESS_STATE_NOT_STARTED = 0;
/**
* @deprecated Use {@link #PROGRESS_STATE_NOT_STARTED} instead.
*/
@Deprecated public static final int PROGRESS_STATE_NO_TRANSFORMATION = PROGRESS_STATE_NOT_STARTED;
/** Indicates that the progress is currently unavailable, but might become available. */
public static final int PROGRESS_STATE_WAITING_FOR_AVAILABILITY = 1;
/** Indicates that the progress is available. */
public static final int PROGRESS_STATE_AVAILABLE = 2;
/** Indicates that the progress is permanently unavailable. */
public static final int PROGRESS_STATE_UNAVAILABLE = 3;
/**
* The default value for the {@linkplain Builder#setMaxDelayBetweenMuxerSamplesMs maximum delay
* between output samples}.
*/
public static final long DEFAULT_MAX_DELAY_BETWEEN_MUXER_SAMPLES_MS =
isRunningOnEmulator() ? 21_000 : 10_000;
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
TRANSFORMER_STATE_PROCESS_FULL_INPUT,
TRANSFORMER_STATE_REMUX_PROCESSED_VIDEO,
TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO,
TRANSFORMER_STATE_PROCESS_AUDIO,
TRANSFORMER_STATE_COPY_OUTPUT,
TRANSFORMER_STATE_PROCESS_MEDIA_START,
TRANSFORMER_STATE_REMUX_REMAINING_MEDIA
})
private @interface TransformerState {}
/** The default Transformer state. */
private static final int TRANSFORMER_STATE_PROCESS_FULL_INPUT = 0;
/**
* The first state of a {@link #resume(Composition composition, String outputFilePath, String
* oldFilePath)} export.
*
* <p>In this state, the paused export file's encoded video track is muxed into a video-only file,
* stored at {@code oldFilePath}.
*
* <p>The video-only file is kept open to allow the {@link
* #TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO} to continue writing to the same file & video track.
*
* <p>A successful operation in this state moves the Transformer to the {@link
* #TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO} state.
*/
private static final int TRANSFORMER_STATE_REMUX_PROCESSED_VIDEO = 1;
/**
* The second state of a {@link #resume(Composition composition, String outputFilePath, String
* oldFilePath)} export.
*
* <p>In this state, the remaining {@link Composition} video data is processed and muxed into the
* same video-only file, stored at {@code oldFilePath}.
*
* <p>A successful operation in this state moves the Transformer to the {@link
* #TRANSFORMER_STATE_PROCESS_AUDIO} state.
*/
private static final int TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO = 2;
/**
* The third state of a {@link #resume(Composition composition, String outputFilePath, String
* oldFilePath)} resumed export.
*
* <p>In this state, the entire {@link Composition} audio is processed and muxed. This same
* operation also transmuxes the video-only file produced by {@link
* #TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO}, interleaving of the audio and video tracks. The
* output is stored at {@code oldFilePath}.
*
* <p>A successful operation in this state moves the Transformer to the {@link
* #TRANSFORMER_STATE_COPY_OUTPUT} state.
*/
private static final int TRANSFORMER_STATE_PROCESS_AUDIO = 3;
/**
* The final state of a {@link #resume(Composition composition, String outputFilePath, String
* oldFilePath)} export.
*
* <p>In this state, the successful exported file (stored at {@code oldFilePath}) is copied to the
* {@code outputFilePath}.
*/
private static final int TRANSFORMER_STATE_COPY_OUTPUT = 4;
private static final int TRANSFORMER_STATE_PROCESS_MEDIA_START = 5;
private static final int TRANSFORMER_STATE_REMUX_REMAINING_MEDIA = 6;
private final Context context;
private final TransformationRequest transformationRequest;
private final ImmutableList<AudioProcessor> audioProcessors;
private final ImmutableList<Effect> videoEffects;
private final boolean removeAudio;
private final boolean removeVideo;
private final boolean flattenForSlowMotion;
private final boolean trimOptimizationEnabled;
private final boolean fileStartsOnVideoFrameEnabled;
private final long maxDelayBetweenMuxerSamplesMs;
private final int maxFramesInEncoder;
private final ListenerSet<Transformer.Listener> listeners;
@Nullable private final AssetLoader.Factory assetLoaderFactory;
private final AudioMixer.Factory audioMixerFactory;
private final VideoFrameProcessor.Factory videoFrameProcessorFactory;
private final Codec.EncoderFactory encoderFactory;
private final Muxer.Factory muxerFactory;
private final Looper looper;
private final DebugViewProvider debugViewProvider;
private final Clock clock;
private final HandlerWrapper applicationHandler;
private final ComponentListener componentListener;
private final ExportResult.Builder exportResultBuilder;
@Nullable private TransformerInternal transformerInternal;
@Nullable private MuxerWrapper remuxingMuxerWrapper;
private @MonotonicNonNull Composition composition;
private @MonotonicNonNull String outputFilePath;
private @MonotonicNonNull String oldFilePath;
private @TransformerState int transformerState;
private TransmuxTranscodeHelper.@MonotonicNonNull ResumeMetadata resumeMetadata;
private @MonotonicNonNull ListenableFuture<TransmuxTranscodeHelper.ResumeMetadata>
getResumeMetadataFuture;
private @MonotonicNonNull ListenableFuture<Void> copyOutputFuture;
@Nullable private Mp4Info mediaItemInfo;
private Transformer(
Context context,
TransformationRequest transformationRequest,
ImmutableList<AudioProcessor> audioProcessors,
ImmutableList<Effect> videoEffects,
boolean removeAudio,
boolean removeVideo,
boolean flattenForSlowMotion,
boolean trimOptimizationEnabled,
boolean fileStartsOnVideoFrameEnabled,
long maxDelayBetweenMuxerSamplesMs,
int maxFramesInEncoder,
ListenerSet<Listener> listeners,
@Nullable AssetLoader.Factory assetLoaderFactory,
AudioMixer.Factory audioMixerFactory,
VideoFrameProcessor.Factory videoFrameProcessorFactory,
Codec.EncoderFactory encoderFactory,
Muxer.Factory muxerFactory,
Looper looper,
DebugViewProvider debugViewProvider,
Clock clock) {
checkState(!removeAudio || !removeVideo, "Audio and video cannot both be removed.");
this.context = context;
this.transformationRequest = transformationRequest;
this.audioProcessors = audioProcessors;
this.videoEffects = videoEffects;
this.removeAudio = removeAudio;
this.removeVideo = removeVideo;
this.flattenForSlowMotion = flattenForSlowMotion;
this.trimOptimizationEnabled = trimOptimizationEnabled;
this.fileStartsOnVideoFrameEnabled = fileStartsOnVideoFrameEnabled;
this.maxDelayBetweenMuxerSamplesMs = maxDelayBetweenMuxerSamplesMs;
this.maxFramesInEncoder = maxFramesInEncoder;
this.listeners = listeners;
this.assetLoaderFactory = assetLoaderFactory;
this.audioMixerFactory = audioMixerFactory;
this.videoFrameProcessorFactory = videoFrameProcessorFactory;
this.encoderFactory = encoderFactory;
this.muxerFactory = muxerFactory;
this.looper = looper;
this.debugViewProvider = debugViewProvider;
this.clock = clock;
transformerState = TRANSFORMER_STATE_PROCESS_FULL_INPUT;
applicationHandler = clock.createHandler(looper, /* callback= */ null);
componentListener = new ComponentListener();
exportResultBuilder = new ExportResult.Builder();
}
/** Returns a {@link Transformer.Builder} initialized with the values of this instance. */
public Builder buildUpon() {
return new Builder(this);
}
/**
* @deprecated Use {@link #addListener(Listener)}, {@link #removeListener(Listener)} or {@link
* #removeAllListeners()} instead.
*/
@Deprecated
public void setListener(Transformer.Listener listener) {
verifyApplicationThread();
this.listeners.clear();
this.listeners.add(listener);
}
/**
* Adds a {@link Transformer.Listener} to listen to the export events.
*
* @param listener A {@link Transformer.Listener}.
* @throws IllegalStateException If this method is called from the wrong thread.
*/
public void addListener(Transformer.Listener listener) {
verifyApplicationThread();
this.listeners.add(listener);
}
/**
* Removes a {@link Transformer.Listener}.
*
* @param listener A {@link Transformer.Listener}.
* @throws IllegalStateException If this method is called from the wrong thread.
*/
public void removeListener(Transformer.Listener listener) {
verifyApplicationThread();
this.listeners.remove(listener);
}
/**
* Removes all {@linkplain Transformer.Listener listeners}.
*
* @throws IllegalStateException If this method is called from the wrong thread.
*/
public void removeAllListeners() {
verifyApplicationThread();
this.listeners.clear();
}
/**
* Starts an asynchronous operation to export the given {@link Composition}.
*
* <p>The first {@link EditedMediaItem} in the first {@link EditedMediaItemSequence} that has a
* given {@linkplain C.TrackType track} will determine the output format for that track, unless
* the format is set when {@linkplain Builder#build building} the {@code Transformer}. For
* example, consider the following composition
*
* <pre>
* Composition {
* EditedMediaItemSequence {
* [ImageMediaItem, VideoMediaItem]
* },
* EditedMediaItemSequence {
* [AudioMediaItem]
* },
* }
* </pre>
*
* The video format will be determined by the {@code ImageMediaItem} in the first {@link
* EditedMediaItemSequence}, while the audio format will be determined by the {@code
* AudioMediaItem} in the second {@code EditedMediaItemSequence}.
*
* <p>This method is under development. A {@link Composition} must meet the following conditions:
*
* <ul>
* <li>The video composition {@link Presentation} effect is applied after input streams are
* composited. Other composition effects are ignored.
* </ul>
*
* <p>{@linkplain EditedMediaItemSequence Sequences} within the {@link Composition} must meet the
* following conditions:
*
* <ul>
* <li>If an {@link EditedMediaItem} in a sequence contains data of a given {@linkplain
* C.TrackType track}, so must all items in that sequence.
* <ul>
* <li>For audio, this condition can be removed by setting an experimental {@link
* Composition.Builder#experimentalSetForceAudioTrack(boolean) flag}.
* </ul>
* <li>If a sequence starts with an HDR {@link EditedMediaItem}, all the following items in the
* sequence must be HDR.
* <li>All sequences containing audio data must output audio with the same {@linkplain
* AudioFormat properties}. This can be done by adding {@linkplain EditedMediaItem#effects
* item specific effects}, such as {@link SonicAudioProcessor} and {@link
* ChannelMixingAudioProcessor}.
* </ul>
*
* <p>The export state is notified through the {@linkplain Builder#addListener(Listener)
* listener}.
*
* <p>Concurrent exports on the same Transformer object are not allowed.
*
* <p>If no custom {@link Transformer.Builder#setMuxerFactory(Muxer.Factory) Muxer.Factory} is
* specified, the output is an MP4 file.
*
* <p>The output can contain at most one video track and one audio track. Other track types are
* ignored. For adaptive bitrate inputs, if no custom {@link
* Transformer.Builder#setAssetLoaderFactory(AssetLoader.Factory) AssetLoader.Factory} is
* specified, the highest bitrate video and audio streams are selected.
*
* <p>If exporting the video track entails transcoding, the output frames' dimensions will be
* swapped if the output video's height is larger than the width. This is to improve compatibility
* among different device encoders.
*
* @param composition The {@link Composition} to export.
* @param path The path to the output file.
* @throws IllegalStateException If this method is called from the wrong thread.
* @throws IllegalStateException If an export is already in progress.
*/
public void start(Composition composition, String path) {
verifyApplicationThread();
initialize(composition, path);
if (!trimOptimizationEnabled || isMultiAsset()) {
startInternal(
composition,
new MuxerWrapper(
path,
muxerFactory,
componentListener,
MuxerWrapper.MUXER_MODE_DEFAULT,
/* dropSamplesBeforeFirstVideoSample= */ fileStartsOnVideoFrameEnabled,
/* appendVideoFormat= */ null,
maxDelayBetweenMuxerSamplesMs),
componentListener,
/* initialTimestampOffsetUs= */ 0,
/* useDefaultAssetLoaderFactory= */ false);
} else {
processMediaBeforeFirstSyncSampleAfterTrimStartTime();
}
}
/**
* Starts an asynchronous operation to export the given {@link EditedMediaItem}.
*
* <p>The export state is notified through the {@linkplain Builder#addListener(Listener)
* listener}.
*
* <p>Concurrent exports on the same Transformer object are not allowed.
*
* <p>If no custom {@link Transformer.Builder#setMuxerFactory(Muxer.Factory) Muxer.Factory} is
* specified, the output is an MP4 file.
*
* <p>The output can contain at most one video track and one audio track. Other track types are
* ignored. For adaptive bitrate inputs, if no custom {@link
* Transformer.Builder#setAssetLoaderFactory(AssetLoader.Factory) AssetLoader.Factory} is
* specified, the highest bitrate video and audio streams are selected.
*
* <p>If exporting the video track entails transcoding, the output frames' dimensions will be
* swapped if the output video's height is larger than the width. This is to improve compatibility
* among different device encoders.
*
* @param editedMediaItem The {@link EditedMediaItem} to export.
* @param path The path to the output file.
* @throws IllegalStateException If this method is called from the wrong thread.
* @throws IllegalStateException If an export is already in progress.
*/
public void start(EditedMediaItem editedMediaItem, String path) {
start(new Composition.Builder(new EditedMediaItemSequence(editedMediaItem)).build(), path);
}
/**
* Starts an asynchronous operation to export the given {@link MediaItem}.
*
* <p>The export state is notified through the {@linkplain Builder#addListener(Listener)
* listener}.
*
* <p>Concurrent exports on the same Transformer object are not allowed.
*
* <p>If no custom {@link Transformer.Builder#setMuxerFactory(Muxer.Factory) Muxer.Factory} is
* specified, the output is an MP4 file.
*
* <p>The output can contain at most one video track and one audio track. Other track types are
* ignored. For adaptive bitrate inputs, if no custom {@link
* Transformer.Builder#setAssetLoaderFactory(AssetLoader.Factory) AssetLoader.Factory} is
* specified, the highest bitrate video and audio streams are selected.
*
* <p>If exporting the video track entails transcoding, the output frames' dimensions will be
* swapped if the output video's height is larger than the width. This is to improve compatibility
* among different device encoders.
*
* @param mediaItem The {@link MediaItem} to export.
* @param path The path to the output file.
* @throws IllegalArgumentException If the {@link MediaItem} is not supported.
* @throws IllegalStateException If this method is called from the wrong thread.
* @throws IllegalStateException If an export is already in progress.
*/
public void start(MediaItem mediaItem, String path) {
if (!mediaItem.clippingConfiguration.equals(MediaItem.ClippingConfiguration.UNSET)
&& flattenForSlowMotion) {
throw new IllegalArgumentException(
"Clipping is not supported when slow motion flattening is requested");
}
EditedMediaItem editedMediaItem =
new EditedMediaItem.Builder(mediaItem)
.setRemoveAudio(removeAudio)
.setRemoveVideo(removeVideo)
.setFlattenForSlowMotion(flattenForSlowMotion)
.setEffects(new Effects(audioProcessors, videoEffects))
.build();
start(editedMediaItem, path);
}
/**
* @deprecated Use {@link #start(MediaItem, String)} instead.
*/
@Deprecated
@InlineMe(replacement = "this.start(mediaItem, path)")
public void startTransformation(MediaItem mediaItem, String path) {
start(mediaItem, path);
}
/**
* Returns the {@link Looper} associated with the application thread that's used to access the
* transformer and on which transformer events are received.
*/
public Looper getApplicationLooper() {
return looper;
}
/**
* Returns the current {@link ProgressState} and updates {@code progressHolder} with the current
* progress if it is {@link #PROGRESS_STATE_AVAILABLE available}.
*
* <p>If the export is {@linkplain #resume(Composition, String, String) resumed}, this method
* returns {@link #PROGRESS_STATE_UNAVAILABLE}.
*
* <p>After an export {@linkplain Listener#onCompleted(Composition, ExportResult) completes}, this
* method returns {@link #PROGRESS_STATE_NOT_STARTED}.
*
* @param progressHolder A {@link ProgressHolder}, updated to hold the percentage progress if
* {@link #PROGRESS_STATE_AVAILABLE available}.
* @return The {@link ProgressState}.
* @throws IllegalStateException If this method is called from the wrong thread.
*/
public @ProgressState int getProgress(ProgressHolder progressHolder) {
verifyApplicationThread();
if (isExportResumed()) {
// Progress updates are unavailable for resumed exports.
return PROGRESS_STATE_UNAVAILABLE;
}
if (isExportTrimOptimization()) {
return getTrimOptimizationProgress(progressHolder);
}
return transformerInternal == null
? PROGRESS_STATE_NOT_STARTED
: transformerInternal.getProgress(progressHolder);
}
private boolean isExportResumed() {
return transformerState == TRANSFORMER_STATE_REMUX_PROCESSED_VIDEO
|| transformerState == TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO
|| transformerState == TRANSFORMER_STATE_PROCESS_AUDIO
|| transformerState == TRANSFORMER_STATE_COPY_OUTPUT;
}
private boolean isExportTrimOptimization() {
return transformerState == TRANSFORMER_STATE_PROCESS_MEDIA_START
|| transformerState == TRANSFORMER_STATE_REMUX_REMAINING_MEDIA;
}
private @ProgressState int getTrimOptimizationProgress(ProgressHolder progressHolder) {
if (mediaItemInfo == null) {
return PROGRESS_STATE_WAITING_FOR_AVAILABILITY;
}
MediaItem firstMediaItem =
checkNotNull(composition).sequences.get(0).editedMediaItems.get(0).mediaItem;
long trimStartTimeUs = firstMediaItem.clippingConfiguration.startPositionUs;
long transcodeDuration = mediaItemInfo.firstSyncSampleTimestampUsAfterTimeUs - trimStartTimeUs;
float transcodeWeighting = (float) transcodeDuration / mediaItemInfo.durationUs;
if (transformerState == TRANSFORMER_STATE_PROCESS_MEDIA_START) {
if (transformerInternal == null) {
return PROGRESS_STATE_WAITING_FOR_AVAILABILITY;
}
@ProgressState
int processMediaStartProgressState = transformerInternal.getProgress(progressHolder);
switch (processMediaStartProgressState) {
case PROGRESS_STATE_NOT_STARTED:
case PROGRESS_STATE_WAITING_FOR_AVAILABILITY:
return PROGRESS_STATE_WAITING_FOR_AVAILABILITY;
case PROGRESS_STATE_AVAILABLE:
progressHolder.progress = round(progressHolder.progress * transcodeWeighting);
return PROGRESS_STATE_AVAILABLE;
case PROGRESS_STATE_UNAVAILABLE:
return PROGRESS_STATE_UNAVAILABLE;
default:
throw new IllegalStateException();
}
}
float fullTranscodeProgress = 100 * transcodeWeighting;
if (transformerInternal == null) {
// Transformer has not started remuxing the remaining media yet.
progressHolder.progress = round(fullTranscodeProgress);
return PROGRESS_STATE_AVAILABLE;
}
@ProgressState
int remuxRemainingMediaProgressState = transformerInternal.getProgress(progressHolder);
switch (remuxRemainingMediaProgressState) {
case PROGRESS_STATE_NOT_STARTED:
case PROGRESS_STATE_WAITING_FOR_AVAILABILITY:
progressHolder.progress = round(fullTranscodeProgress);
return PROGRESS_STATE_AVAILABLE;
case PROGRESS_STATE_AVAILABLE:
progressHolder.progress =
round(fullTranscodeProgress + (1 - transcodeWeighting) * progressHolder.progress);
return PROGRESS_STATE_AVAILABLE;
case PROGRESS_STATE_UNAVAILABLE:
return PROGRESS_STATE_UNAVAILABLE;
default:
throw new IllegalStateException();
}
}
/**
* Cancels the export that is currently in progress, if any.
*
* <p>The export output file (if any) is not deleted.
*
* @throws IllegalStateException If this method is called from the wrong thread.
*/
public void cancel() {
verifyApplicationThread();
if (transformerInternal == null) {
return;
}
try {
transformerInternal.cancel();
} finally {
transformerInternal = null;
}
if (getResumeMetadataFuture != null && !getResumeMetadataFuture.isDone()) {
getResumeMetadataFuture.cancel(/* mayInterruptIfRunning= */ false);
}
if (copyOutputFuture != null && !copyOutputFuture.isDone()) {
copyOutputFuture.cancel(/* mayInterruptIfRunning= */ false);
}
}
/**
* Resumes a previously {@linkplain #cancel() cancelled} export.
*
* <p>An export can be resumed only when:
*
* <ul>
* <li>The {@link Composition} contains a single {@link EditedMediaItemSequence} having
* continuous audio and video tracks.
* <li>The output is an MP4 file.
* </ul>
*
* <p>Note that export optimizations (such as {@linkplain
* Builder#experimentalSetTrimOptimizationEnabled trim optimization}) will not be applied upon
* resumption.
*
* @param composition The {@link Composition} to resume export.
* @param outputFilePath The path to the output file. This must be different from the output path
* of the cancelled export.
* @param oldFilePath The output path of the the cancelled export.
*/
public void resume(Composition composition, String outputFilePath, String oldFilePath) {
verifyApplicationThread();
initialize(composition, outputFilePath);
this.oldFilePath = oldFilePath;
remuxProcessedVideo();
}
private void initialize(Composition composition, String outputFilePath) {
this.composition = composition;
this.outputFilePath = outputFilePath;
exportResultBuilder.reset();
}
private void processFullInput() {
transformerState = TRANSFORMER_STATE_PROCESS_FULL_INPUT;
startInternal(
checkNotNull(composition),
new MuxerWrapper(
checkNotNull(outputFilePath),
muxerFactory,
componentListener,
MuxerWrapper.MUXER_MODE_DEFAULT,
/* dropSamplesBeforeFirstVideoSample= */ false,
/* appendVideoFormat= */ null,
maxDelayBetweenMuxerSamplesMs),
componentListener,
/* initialTimestampOffsetUs= */ 0,
/* useDefaultAssetLoaderFactory= */ false);
}
private void remuxProcessedVideo() {
transformerState = TRANSFORMER_STATE_REMUX_PROCESSED_VIDEO;
getResumeMetadataFuture =
TransmuxTranscodeHelper.getResumeMetadataAsync(
context, checkNotNull(oldFilePath), checkNotNull(composition));
Futures.addCallback(
getResumeMetadataFuture,
new FutureCallback<TransmuxTranscodeHelper.ResumeMetadata>() {
@Override
public void onSuccess(TransmuxTranscodeHelper.ResumeMetadata resumeMetadata) {
// If there is no video track to remux or the last sync sample is actually the first
// sample, then start the normal Export.
if (resumeMetadata.lastSyncSampleTimestampUs == C.TIME_UNSET
|| resumeMetadata.lastSyncSampleTimestampUs == 0) {
processFullInput();
return;
}
Transformer.this.resumeMetadata = resumeMetadata;
remuxingMuxerWrapper =
new MuxerWrapper(
checkNotNull(outputFilePath),
muxerFactory,
componentListener,
MuxerWrapper.MUXER_MODE_MUX_PARTIAL,
/* dropSamplesBeforeFirstVideoSample= */ false,
/* appendVideoFormat= */ resumeMetadata.videoFormat,
maxDelayBetweenMuxerSamplesMs);
startInternal(
TransmuxTranscodeHelper.createVideoOnlyComposition(
oldFilePath,
/* clippingEndPositionUs= */ resumeMetadata.lastSyncSampleTimestampUs),
checkNotNull(remuxingMuxerWrapper),
componentListener,
/* initialTimestampOffsetUs= */ 0,
/* useDefaultAssetLoaderFactory= */ true);
}
@Override
public void onFailure(Throwable t) {
// In case of error fallback to normal Export.
processFullInput();
}
},
applicationHandler::post);
}
private void processRemainingVideo() {
transformerState = TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO;
Composition videoOnlyComposition =
TransmuxTranscodeHelper.buildUponComposition(
checkNotNull(composition),
/* removeAudio= */ true,
/* removeVideo= */ false,
resumeMetadata);
checkNotNull(remuxingMuxerWrapper);
remuxingMuxerWrapper.changeToAppendMode();
startInternal(
videoOnlyComposition,
remuxingMuxerWrapper,
componentListener,
/* initialTimestampOffsetUs= */ checkNotNull(resumeMetadata).lastSyncSampleTimestampUs,
/* useDefaultAssetLoaderFactory= */ false);
}
private void processAudio() {
transformerState = TRANSFORMER_STATE_PROCESS_AUDIO;
MuxerWrapper muxerWrapper =
new MuxerWrapper(
checkNotNull(oldFilePath),
muxerFactory,
componentListener,
MuxerWrapper.MUXER_MODE_DEFAULT,
/* dropSamplesBeforeFirstVideoSample= */ false,
/* appendVideoFormat= */ null,
maxDelayBetweenMuxerSamplesMs);
startInternal(
TransmuxTranscodeHelper.createAudioTranscodeAndVideoTransmuxComposition(
checkNotNull(composition), checkNotNull(outputFilePath)),
muxerWrapper,
componentListener,
/* initialTimestampOffsetUs= */ 0,
/* useDefaultAssetLoaderFactory= */ false);
}
// TODO: b/308253384 - Move copy output logic into MuxerWrapper.
private void copyOutput() {
transformerState = TRANSFORMER_STATE_COPY_OUTPUT;
copyOutputFuture =
TransmuxTranscodeHelper.copyFileAsync(
new File(checkNotNull(oldFilePath)), new File(checkNotNull(outputFilePath)));
Futures.addCallback(
copyOutputFuture,
new FutureCallback<Void>() {
@Override
public void onSuccess(Void result) {
onExportCompletedWithSuccess();
}
@Override
public void onFailure(Throwable t) {
onExportCompletedWithError(
ExportException.createForUnexpected(
new IOException("Copy output task failed for the resumed export", t)));
}
},
applicationHandler::post);
}
private void processMediaBeforeFirstSyncSampleAfterTrimStartTime() {
transformerState = TRANSFORMER_STATE_PROCESS_MEDIA_START;
EditedMediaItem firstEditedMediaItem =
checkNotNull(composition).sequences.get(0).editedMediaItems.get(0);
long trimStartTimeUs = firstEditedMediaItem.mediaItem.clippingConfiguration.startPositionUs;
long trimEndTimeUs = firstEditedMediaItem.mediaItem.clippingConfiguration.endPositionUs;
ListenableFuture<Mp4Info> getMp4InfoFuture =
TransmuxTranscodeHelper.getMp4Info(
context,
checkNotNull(firstEditedMediaItem.mediaItem.localConfiguration).uri.toString(),
trimStartTimeUs);
Futures.addCallback(
getMp4InfoFuture,
new FutureCallback<Mp4Info>() {
@Override
public void onSuccess(Mp4Info mp4Info) {
if (mp4Info.firstSyncSampleTimestampUsAfterTimeUs == C.TIME_UNSET) {
exportResultBuilder.setOptimizationResult(OPTIMIZATION_ABANDONED_OTHER);
processFullInput();
return;
}
if (mp4Info.firstSyncSampleTimestampUsAfterTimeUs == C.TIME_END_OF_SOURCE
|| (trimEndTimeUs != C.TIME_END_OF_SOURCE
&& trimEndTimeUs < mp4Info.firstSyncSampleTimestampUsAfterTimeUs)) {
exportResultBuilder.setOptimizationResult(
OPTIMIZATION_ABANDONED_KEYFRAME_PLACEMENT_OPTIMAL_FOR_TRIM);
processFullInput();
return;
}
long maxEncodedAudioBufferDurationUs = 0;
if (mp4Info.audioFormat != null && mp4Info.audioFormat.sampleRate != Format.NO_VALUE) {
// Ensure there is an audio sample to mux between the two clip times to prevent
// Transformer from hanging because it received an audio track but no audio samples.
maxEncodedAudioBufferDurationUs =
Util.sampleCountToDurationUs(
AAC_LC_AUDIO_SAMPLE_COUNT, mp4Info.audioFormat.sampleRate);
}
if (mp4Info.firstSyncSampleTimestampUsAfterTimeUs - trimStartTimeUs
<= maxEncodedAudioBufferDurationUs
|| mp4Info.isFirstVideoSampleAfterTimeUsSyncSample) {
Transformer.this.composition =
buildUponCompositionForTrimOptimization(
composition,
mp4Info.firstSyncSampleTimestampUsAfterTimeUs,
trimEndTimeUs,
mp4Info.durationUs,
/* startsAtKeyFrame= */ true,
/* clearVideoEffects= */ false);
exportResultBuilder.setOptimizationResult(
OPTIMIZATION_ABANDONED_KEYFRAME_PLACEMENT_OPTIMAL_FOR_TRIM);
processFullInput();
return;
}
remuxingMuxerWrapper =
new MuxerWrapper(
checkNotNull(outputFilePath),
muxerFactory,
componentListener,
MuxerWrapper.MUXER_MODE_MUX_PARTIAL,
/* dropSamplesBeforeFirstVideoSample= */ false,
mp4Info.videoFormat,
maxDelayBetweenMuxerSamplesMs);
if (shouldTranscodeVideo(
checkNotNull(mp4Info.videoFormat),
composition,
/* sequenceIndex= */ 0,
transformationRequest,
encoderFactory,
remuxingMuxerWrapper)
|| (mp4Info.audioFormat != null
&& shouldTranscodeAudio(
mp4Info.audioFormat,
composition,
/* sequenceIndex= */ 0,
transformationRequest,
encoderFactory,
remuxingMuxerWrapper))) {
remuxingMuxerWrapper = null;
exportResultBuilder.setOptimizationResult(
OPTIMIZATION_ABANDONED_TRIM_AND_TRANSCODING_TRANSFORMATION_REQUESTED);
processFullInput();
return;
}
Transformer.this.mediaItemInfo = mp4Info;
maybeSetMuxerWrapperAdditionalRotationDegrees(
remuxingMuxerWrapper,
firstEditedMediaItem.effects.videoEffects,
checkNotNull(mp4Info.videoFormat));
Composition trancodeComposition =
buildUponCompositionForTrimOptimization(
composition,
trimStartTimeUs,
mp4Info.firstSyncSampleTimestampUsAfterTimeUs,
mp4Info.durationUs,
/* startsAtKeyFrame= */ false,
/* clearVideoEffects= */ true);
startInternal(
trancodeComposition,
checkNotNull(remuxingMuxerWrapper),
componentListener,
/* initialTimestampOffsetUs= */ 0,
/* useDefaultAssetLoaderFactory= */ false);
}
@Override
public void onFailure(Throwable t) {
exportResultBuilder.setOptimizationResult(OPTIMIZATION_FAILED_EXTRACTION_FAILED);
processFullInput();
}
},
applicationHandler::post);
}
private void remuxRemainingMedia() {
transformerState = TRANSFORMER_STATE_REMUX_REMAINING_MEDIA;
EditedMediaItem firstEditedMediaItem =
checkNotNull(composition).sequences.get(0).editedMediaItems.get(0);
Mp4Info mediaItemInfo = checkNotNull(this.mediaItemInfo);
long trimStartTimeUs = firstEditedMediaItem.mediaItem.clippingConfiguration.startPositionUs;
long trimEndTimeUs = firstEditedMediaItem.mediaItem.clippingConfiguration.endPositionUs;
Composition transmuxComposition =
buildUponCompositionForTrimOptimization(
composition,
mediaItemInfo.firstSyncSampleTimestampUsAfterTimeUs,
trimEndTimeUs,
mediaItemInfo.durationUs,
/* startsAtKeyFrame= */ true,
/* clearVideoEffects= */ true);
checkNotNull(remuxingMuxerWrapper);
remuxingMuxerWrapper.changeToAppendMode();
startInternal(
transmuxComposition,
remuxingMuxerWrapper,
componentListener,
/* initialTimestampOffsetUs= */ mediaItemInfo.firstSyncSampleTimestampUsAfterTimeUs
- trimStartTimeUs,
/* useDefaultAssetLoaderFactory= */ false);
}
private boolean isMultiAsset() {
return checkNotNull(composition).sequences.size() > 1
|| composition.sequences.get(0).editedMediaItems.size() > 1;
}
private void verifyApplicationThread() {
if (Looper.myLooper() != looper) {
throw new IllegalStateException("Transformer is accessed on the wrong thread.");
}
}
private void startInternal(
Composition composition,
MuxerWrapper muxerWrapper,
ComponentListener componentListener,
long initialTimestampOffsetUs,
boolean useDefaultAssetLoaderFactory) {
checkState(transformerInternal == null, "There is already an export in progress.");
TransformationRequest transformationRequest = this.transformationRequest;
if (composition.hdrMode != Composition.HDR_MODE_KEEP_HDR) {
transformationRequest =
transformationRequest.buildUpon().setHdrMode(composition.hdrMode).build();
}
FallbackListener fallbackListener =
new FallbackListener(composition, listeners, applicationHandler, transformationRequest);
AssetLoader.Factory assetLoaderFactory = this.assetLoaderFactory;
if (useDefaultAssetLoaderFactory || assetLoaderFactory == null) {
assetLoaderFactory =
new DefaultAssetLoaderFactory(context, new DefaultDecoderFactory(context), clock);
}
DebugTraceUtil.reset();
transformerInternal =
new TransformerInternal(
context,
composition,
transformationRequest,
assetLoaderFactory,
audioMixerFactory,
videoFrameProcessorFactory,
encoderFactory,
maxFramesInEncoder,
muxerWrapper,
componentListener,
fallbackListener,
applicationHandler,
debugViewProvider,
clock,
initialTimestampOffsetUs);
transformerInternal.start();
}
private void onExportCompletedWithSuccess() {
listeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener -> listener.onCompleted(checkNotNull(composition), exportResultBuilder.build()));
listeners.flushEvents();
transformerState = TRANSFORMER_STATE_PROCESS_FULL_INPUT;
}
private void onExportCompletedWithError(ExportException exception) {
listeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener ->
listener.onError(checkNotNull(composition), exportResultBuilder.build(), exception));
listeners.flushEvents();
transformerState = TRANSFORMER_STATE_PROCESS_FULL_INPUT;
}
private final class ComponentListener
implements TransformerInternal.Listener, MuxerWrapper.Listener {
// TransformerInternal.Listener implementation
@Override
public void onCompleted(
ImmutableList<ExportResult.ProcessedInput> processedInputs,
@Nullable String audioEncoderName,
@Nullable String videoEncoderName) {
exportResultBuilder.addProcessedInputs(processedInputs);
// When an export is resumed, the audio and video encoder name (if any) can comes from
// different intermittent exports, so set encoder names only when they are available.
if (audioEncoderName != null) {
exportResultBuilder.setAudioEncoderName(audioEncoderName);
}
if (videoEncoderName != null) {
exportResultBuilder.setVideoEncoderName(videoEncoderName);
}
// TODO(b/213341814): Add event flags for Transformer events.
transformerInternal = null;
if (transformerState == TRANSFORMER_STATE_REMUX_PROCESSED_VIDEO) {
processRemainingVideo();
} else if (transformerState == TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO) {
remuxingMuxerWrapper = null;
processAudio();
} else if (transformerState == TRANSFORMER_STATE_PROCESS_AUDIO) {
copyOutput();
} else if (transformerState == TRANSFORMER_STATE_PROCESS_MEDIA_START) {
remuxRemainingMedia();
} else if (transformerState == TRANSFORMER_STATE_REMUX_REMAINING_MEDIA) {
mediaItemInfo = null;
exportResultBuilder.setOptimizationResult(ExportResult.OPTIMIZATION_SUCCEEDED);
onExportCompletedWithSuccess();
} else {
onExportCompletedWithSuccess();
}
}
@Override
@SuppressWarnings("UngroupedOverloads") // Grouped by interface.
public void onError(
ImmutableList<ExportResult.ProcessedInput> processedInputs,
@Nullable String audioEncoderName,
@Nullable String videoEncoderName,
ExportException exportException) {
if (exportException.errorCode == ERROR_CODE_MUXING_APPEND
&& (isExportTrimOptimization() || isExportResumed())) {
remuxingMuxerWrapper = null;
transformerInternal = null;
exportResultBuilder.reset();
exportResultBuilder.setOptimizationResult(OPTIMIZATION_FAILED_FORMAT_MISMATCH);
processFullInput();
return;
}
exportResultBuilder.addProcessedInputs(processedInputs);
// When an export is resumed, the audio and video encoder name (if any) can comes from
// different intermittent exports, so set encoder names only when they are available.
if (audioEncoderName != null) {
exportResultBuilder.setAudioEncoderName(audioEncoderName);
}
if (videoEncoderName != null) {
exportResultBuilder.setVideoEncoderName(videoEncoderName);
}
exportResultBuilder.setExportException(exportException);
transformerInternal = null;
onExportCompletedWithError(exportException);
}
// MuxerWrapper.Listener implementation
@Override
public void onTrackEnded(
@C.TrackType int trackType, Format format, int averageBitrate, int sampleCount) {
if (trackType == C.TRACK_TYPE_AUDIO) {
exportResultBuilder
.setAudioMimeType(format.sampleMimeType)
.setAverageAudioBitrate(averageBitrate);
if (format.channelCount != Format.NO_VALUE) {
exportResultBuilder.setChannelCount(format.channelCount);
}
if (format.sampleRate != Format.NO_VALUE) {
exportResultBuilder.setSampleRate(format.sampleRate);
}
} else if (trackType == C.TRACK_TYPE_VIDEO) {
exportResultBuilder
.setVideoMimeType(format.sampleMimeType)
.setAverageVideoBitrate(averageBitrate)
.setColorInfo(format.colorInfo)
.setVideoFrameCount(sampleCount);
if (format.height != Format.NO_VALUE) {
exportResultBuilder.setHeight(format.height);
}
if (format.width != Format.NO_VALUE) {
exportResultBuilder.setWidth(format.width);
}
}
}
@Override
public void onEnded(long durationMs, long fileSizeBytes) {
exportResultBuilder.setDurationMs(durationMs).setFileSizeBytes(fileSizeBytes);
checkNotNull(transformerInternal).endWithCompletion();
}
@Override
@SuppressWarnings("UngroupedOverloads") // Grouped by interface.
public void onError(ExportException exportException) {
checkNotNull(transformerInternal).endWithException(exportException);
}
}
}