/*
* 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.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.DebugViewProvider;
import androidx.media3.common.Effect;
import androidx.media3.common.FrameProcessor;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.effect.GlEffect;
import androidx.media3.effect.GlEffectsFrameProcessor;
import androidx.media3.effect.GlMatrixTransformation;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.extractor.DefaultExtractorsFactory;
import androidx.media3.extractor.mp4.Mp4Extractor;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.File;
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 transform media inputs.
*
* <p>The same Transformer instance can be used to transform 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 TransformationRequest transformationRequest;
private ImmutableList<Effect> videoEffects;
private boolean removeAudio;
private boolean removeVideo;
private ListenerSet<Transformer.Listener> listeners;
private MediaSource.@MonotonicNonNull Factory mediaSourceFactory;
private Codec.DecoderFactory decoderFactory;
private Codec.EncoderFactory encoderFactory;
private FrameProcessor.Factory frameProcessorFactory;
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();
transformationRequest = new TransformationRequest.Builder().build();
videoEffects = ImmutableList.of();
decoderFactory = new DefaultDecoderFactory(this.context);
encoderFactory = new DefaultEncoderFactory.Builder(this.context).build();
frameProcessorFactory = new GlEffectsFrameProcessor.Factory();
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.transformationRequest = transformer.transformationRequest;
this.videoEffects = transformer.videoEffects;
this.removeAudio = transformer.removeAudio;
this.removeVideo = transformer.removeVideo;
this.listeners = transformer.listeners;
this.mediaSourceFactory = transformer.mediaSourceFactory;
this.decoderFactory = transformer.decoderFactory;
this.encoderFactory = transformer.encoderFactory;
this.frameProcessorFactory = transformer.frameProcessorFactory;
this.muxerFactory = transformer.muxerFactory;
this.looper = transformer.looper;
this.debugViewProvider = transformer.debugViewProvider;
this.clock = transformer.clock;
}
/**
* Sets the {@link TransformationRequest} which configures the editing and transcoding options.
*
* <p>Actual applied values may differ, per device capabilities. {@link
* Listener#onFallbackApplied(MediaItem, TransformationRequest, TransformationRequest)} will be
* invoked with the actual applied values.
*
* @param transformationRequest The {@link TransformationRequest}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setTransformationRequest(TransformationRequest transformationRequest) {
this.transformationRequest = transformationRequest;
return this;
}
/**
* Sets the {@link Effect} instances to apply to each video frame.
*
* <p>The {@link Effect} instances are applied before any {@linkplain
* TransformationRequest.Builder#setScale(float, float) scale}, {@linkplain
* TransformationRequest.Builder#setRotationDegrees(float) rotation}, or {@linkplain
* TransformationRequest.Builder#setResolution(int) resolution} changes specified in the {@link
* #setTransformationRequest(TransformationRequest) TransformationRequest} but after {@linkplain
* TransformationRequest.Builder#setFlattenForSlowMotion(boolean) slow-motion flattening}.
*
* <p>The default {@link FrameProcessor} only supports {@link GlEffect} instances. To use other
* effects, call {@link #setFrameProcessorFactory(FrameProcessor.Factory)} with a custom {@link
* FrameProcessor.Factory}.
*
* @param effects The {@link Effect} instances to apply to each video frame.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setVideoEffects(List<Effect> effects) {
this.videoEffects = ImmutableList.copyOf(effects);
return this;
}
/**
* Sets whether to remove the audio from the output.
*
* <p>The default value is {@code false}.
*
* <p>The audio and video cannot both be removed because the output would not contain any
* samples.
*
* @param removeAudio Whether to remove the audio.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setRemoveAudio(boolean removeAudio) {
this.removeAudio = removeAudio;
return this;
}
/**
* Sets whether to remove the video from the output.
*
* <p>The default value is {@code false}.
*
* <p>The audio and video cannot both be removed because the output would not contain any
* samples.
*
* @param removeVideo Whether to remove the video.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setRemoveVideo(boolean removeVideo) {
this.removeVideo = removeVideo;
return this;
}
/**
* @deprecated Use {@link TransformationRequest.Builder#setFlattenForSlowMotion(boolean)}
* instead.
*/
@CanIgnoreReturnValue
@Deprecated
public Builder setFlattenForSlowMotion(boolean flattenForSlowMotion) {
transformationRequest =
transformationRequest.buildUpon().setFlattenForSlowMotion(flattenForSlowMotion).build();
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 transformation 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 MediaSource.Factory} to be used to retrieve the inputs to transform.
*
* <p>The default value is a {@link DefaultMediaSourceFactory} built with the context provided
* in {@linkplain #Builder(Context) the constructor}.
*
* @param mediaSourceFactory A {@link MediaSource.Factory}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMediaSourceFactory(MediaSource.Factory mediaSourceFactory) {
this.mediaSourceFactory = mediaSourceFactory;
return this;
}
/**
* Sets the {@link Codec.DecoderFactory} that will be used by the transformer.
*
* <p>The default value is a {@link DefaultDecoderFactory} instance.
*
* @param decoderFactory The {@link Codec.DecoderFactory} instance.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setDecoderFactory(Codec.DecoderFactory decoderFactory) {
this.decoderFactory = decoderFactory;
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 FrameProcessor.Factory} for the {@link FrameProcessor} to use when applying
* {@linkplain Effect effects} to the video frames.
*
* <p>This factory will be used to create the {@link FrameProcessor} used for applying the
* {@link Effect} instances passed to {@link #setVideoEffects(List)} and any additional {@link
* GlMatrixTransformation} instances derived from the {@link TransformationRequest} set using
* {@link #setTransformationRequest(TransformationRequest)}.
*
* <p>The default is {@link GlEffectsFrameProcessor.Factory}.
*
* @param frameProcessorFactory The {@link FrameProcessor.Factory} to use.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setFrameProcessorFactory(FrameProcessor.Factory frameProcessorFactory) {
this.frameProcessorFactory = frameProcessorFactory;
return this;
}
/**
* Sets the 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
* transformation.
*
* <p>This is intended for debugging. The default value is {@link DebugViewProvider#NONE}, which
* doesn't show any debug info.
*
* <p>Not all transformations 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 NullPointerException If the {@link Context} has not been provided.
* @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 MIME type.
* @throws IllegalStateException If the muxer doesn't support the requested video MIME type.
*/
public Transformer build() {
// TODO(huangdarwin): Remove this checkNotNull after deprecated {@link #setContext(Context)}
// is removed.
checkNotNull(context);
if (transformationRequest.audioMimeType != null) {
checkSampleMimeType(transformationRequest.audioMimeType);
}
if (transformationRequest.videoMimeType != null) {
checkSampleMimeType(transformationRequest.videoMimeType);
}
if (mediaSourceFactory == null) {
DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory();
if (transformationRequest.flattenForSlowMotion) {
defaultExtractorsFactory.setMp4ExtractorFlags(Mp4Extractor.FLAG_READ_SEF_DATA);
}
mediaSourceFactory = new DefaultMediaSourceFactory(context, defaultExtractorsFactory);
}
return new Transformer(
context,
transformationRequest,
videoEffects,
removeAudio,
removeVideo,
listeners,
mediaSourceFactory,
decoderFactory,
encoderFactory,
frameProcessorFactory,
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 transformation events. */
public interface Listener {
/**
* @deprecated Use {@link #onTransformationCompleted(MediaItem, TransformationResult)} instead.
*/
@Deprecated
default void onTransformationCompleted(MediaItem inputMediaItem) {}
/**
* Called when the transformation is completed successfully.
*
* @param inputMediaItem The {@link MediaItem} for which the transformation is completed.
* @param transformationResult The {@link TransformationResult} of the transformation.
*/
default void onTransformationCompleted(
MediaItem inputMediaItem, TransformationResult transformationResult) {
onTransformationCompleted(inputMediaItem);
}
/**
* @deprecated Use {@link #onTransformationError(MediaItem, TransformationException)}.
*/
@Deprecated
default void onTransformationError(MediaItem inputMediaItem, Exception exception) {
onTransformationError(inputMediaItem, (TransformationException) exception);
}
/**
* Called if an exception occurs during the transformation.
*
* @param inputMediaItem The {@link MediaItem} for which the exception occurs.
* @param exception The {@link TransformationException} describing the exception.
*/
default void onTransformationError(
MediaItem inputMediaItem, TransformationException exception) {}
/**
* Called when fallback to an alternative {@link TransformationRequest} is necessary to comply
* with muxer or device constraints.
*
* @param inputMediaItem The {@link MediaItem} for which the transformation 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#enableRequestSdrToneMapping} values set.
*/
default void onFallbackApplied(
MediaItem inputMediaItem,
TransformationRequest originalTransformationRequest,
TransformationRequest fallbackTransformationRequest) {}
}
/**
* Progress state. One of {@link #PROGRESS_STATE_WAITING_FOR_AVAILABILITY}, {@link
* #PROGRESS_STATE_AVAILABLE}, {@link #PROGRESS_STATE_UNAVAILABLE}, {@link
* #PROGRESS_STATE_NO_TRANSFORMATION}
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
PROGRESS_STATE_WAITING_FOR_AVAILABILITY,
PROGRESS_STATE_AVAILABLE,
PROGRESS_STATE_UNAVAILABLE,
PROGRESS_STATE_NO_TRANSFORMATION
})
public @interface ProgressState {}
/**
* Indicates that the progress is unavailable for the current transformation, but might become
* available.
*/
public static final int PROGRESS_STATE_WAITING_FOR_AVAILABILITY = 0;
/** Indicates that the progress is available. */
public static final int PROGRESS_STATE_AVAILABLE = 1;
/** Indicates that the progress is permanently unavailable for the current transformation. */
public static final int PROGRESS_STATE_UNAVAILABLE = 2;
/** Indicates that there is no current transformation. */
public static final int PROGRESS_STATE_NO_TRANSFORMATION = 4;
@VisibleForTesting /* package */ final Codec.DecoderFactory decoderFactory;
@VisibleForTesting /* package */ final Codec.EncoderFactory encoderFactory;
private final Context context;
private final TransformationRequest transformationRequest;
private final ImmutableList<Effect> videoEffects;
private final boolean removeAudio;
private final boolean removeVideo;
private final ListenerSet<Transformer.Listener> listeners;
private final MediaSource.Factory mediaSourceFactory;
private final FrameProcessor.Factory frameProcessorFactory;
private final Muxer.Factory muxerFactory;
private final Looper looper;
private final DebugViewProvider debugViewProvider;
private final Clock clock;
private final ExoPlayerAssetLoader exoPlayerAssetLoader;
@Nullable private MuxerWrapper muxerWrapper;
@Nullable private String outputPath;
@Nullable private ParcelFileDescriptor outputParcelFileDescriptor;
private boolean transformationInProgress;
private boolean isCancelling;
private Transformer(
Context context,
TransformationRequest transformationRequest,
ImmutableList<Effect> videoEffects,
boolean removeAudio,
boolean removeVideo,
ListenerSet<Transformer.Listener> listeners,
MediaSource.Factory mediaSourceFactory,
Codec.DecoderFactory decoderFactory,
Codec.EncoderFactory encoderFactory,
FrameProcessor.Factory frameProcessorFactory,
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.videoEffects = videoEffects;
this.removeAudio = removeAudio;
this.removeVideo = removeVideo;
this.listeners = listeners;
this.mediaSourceFactory = mediaSourceFactory;
this.decoderFactory = decoderFactory;
this.encoderFactory = encoderFactory;
this.frameProcessorFactory = frameProcessorFactory;
this.muxerFactory = muxerFactory;
this.looper = looper;
this.debugViewProvider = debugViewProvider;
this.clock = clock;
exoPlayerAssetLoader =
new ExoPlayerAssetLoader(
context,
transformationRequest,
videoEffects,
removeAudio,
removeVideo,
mediaSourceFactory,
decoderFactory,
encoderFactory,
frameProcessorFactory,
looper,
debugViewProvider,
clock);
}
/** 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 transformation 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 transform the given {@link MediaItem}.
*
* <p>The transformation state is notified through the {@linkplain Builder#addListener(Listener)
* listener}.
*
* <p>Concurrent transformations on the same Transformer object are not allowed.
*
* <p>The output is an MP4 file. It can contain at most one video track and one audio track. Other
* track types are ignored. For adaptive bitrate {@linkplain MediaSource media sources}, the
* highest bitrate video and audio streams are selected.
*
* @param mediaItem The {@link MediaItem} to transform.
* @param path The path to the output file.
* @throws IllegalArgumentException If the path is invalid.
* @throws IllegalArgumentException If the {@link MediaItem} is not supported.
* @throws IllegalStateException If this method is called from the wrong thread.
* @throws IllegalStateException If a transformation is already in progress.
*/
public void startTransformation(MediaItem mediaItem, String path) {
this.outputPath = path;
this.outputParcelFileDescriptor = null;
startTransformationInternal(mediaItem);
}
/**
* Starts an asynchronous operation to transform the given {@link MediaItem}.
*
* <p>The transformation state is notified through the {@linkplain Builder#addListener(Listener)
* listener}.
*
* <p>Concurrent transformations on the same Transformer object are not allowed.
*
* <p>The output is an MP4 file. It can contain at most one video track and one audio track. Other
* track types are ignored. For adaptive bitrate {@linkplain MediaSource media sources}, the
* highest bitrate video and audio streams are selected.
*
* @param mediaItem The {@link MediaItem} to transform.
* @param parcelFileDescriptor A readable and writable {@link ParcelFileDescriptor} of the output.
* The file referenced by this ParcelFileDescriptor should not be used before the
* transformation is completed. It is the responsibility of the caller to close the
* ParcelFileDescriptor. This can be done after this method returns.
* @throws IllegalArgumentException If the file descriptor is invalid.
* @throws IllegalArgumentException If the {@link MediaItem} is not supported.
* @throws IllegalStateException If this method is called from the wrong thread.
* @throws IllegalStateException If a transformation is already in progress.
*/
@RequiresApi(26)
public void startTransformation(MediaItem mediaItem, ParcelFileDescriptor parcelFileDescriptor) {
this.outputParcelFileDescriptor = parcelFileDescriptor;
this.outputPath = null;
startTransformationInternal(mediaItem);
}
private void startTransformationInternal(MediaItem mediaItem) {
if (!mediaItem.clippingConfiguration.equals(MediaItem.ClippingConfiguration.UNSET)
&& transformationRequest.flattenForSlowMotion) {
// TODO(b/233986762): Support clipping with SEF flattening.
throw new IllegalArgumentException(
"Clipping is not supported when slow motion flattening is requested");
}
verifyApplicationThread();
if (transformationInProgress) {
throw new IllegalStateException("There is already a transformation in progress.");
}
transformationInProgress = true;
ComponentListener componentListener = new ComponentListener(mediaItem, looper);
MuxerWrapper muxerWrapper =
new MuxerWrapper(
outputPath,
outputParcelFileDescriptor,
muxerFactory,
/* asyncErrorListener= */ componentListener);
this.muxerWrapper = muxerWrapper;
FallbackListener fallbackListener =
new FallbackListener(
mediaItem,
listeners,
clock.createHandler(looper, /* callback= */ null),
transformationRequest);
exoPlayerAssetLoader.start(
mediaItem,
muxerWrapper,
/* listener= */ componentListener,
fallbackListener,
/* asyncErrorListener= */ componentListener);
}
/**
* 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>After a transformation {@linkplain Listener#onTransformationCompleted(MediaItem,
* TransformationResult) completes}, this method returns {@link
* #PROGRESS_STATE_NO_TRANSFORMATION}.
*
* @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();
return exoPlayerAssetLoader.getProgress(progressHolder);
}
/**
* Cancels the transformation that is currently in progress, if any.
*
* @throws IllegalStateException If this method is called from the wrong thread.
*/
public void cancel() {
verifyApplicationThread();
isCancelling = true;
try {
releaseResources(/* forCancellation= */ true);
} catch (TransformationException impossible) {
throw new IllegalStateException(impossible);
}
isCancelling = false;
}
/**
* Releases the resources.
*
* @param forCancellation Whether the reason for releasing the resources is the transformation
* cancellation.
* @throws IllegalStateException If this method is called from the wrong thread.
* @throws TransformationException If the muxer is in the wrong state and {@code forCancellation}
* is false.
*/
private void releaseResources(boolean forCancellation) throws TransformationException {
transformationInProgress = false;
exoPlayerAssetLoader.release();
if (muxerWrapper != null) {
try {
muxerWrapper.release(forCancellation);
} catch (Muxer.MuxerException e) {
throw TransformationException.createForMuxer(
e, TransformationException.ERROR_CODE_MUXING_FAILED);
}
muxerWrapper = null;
}
}
private void verifyApplicationThread() {
if (Looper.myLooper() != looper) {
throw new IllegalStateException("Transformer is accessed on the wrong thread.");
}
}
/**
* Returns the current size in bytes of the current/latest output file, or {@link C#LENGTH_UNSET}
* if unavailable.
*/
private long getCurrentOutputFileCurrentSizeBytes() {
long fileSize = C.LENGTH_UNSET;
if (outputPath != null) {
fileSize = new File(outputPath).length();
} else if (outputParcelFileDescriptor != null) {
fileSize = outputParcelFileDescriptor.getStatSize();
}
if (fileSize <= 0) {
fileSize = C.LENGTH_UNSET;
}
return fileSize;
}
/** Listener for exceptions that occur during a transformation. */
/* package */ interface AsyncErrorListener {
/**
* Called when a {@link TransformationException} occurs.
*
* <p>Can be called from any thread.
*/
void onTransformationException(TransformationException exception);
}
private final class ComponentListener
implements ExoPlayerAssetLoader.Listener, AsyncErrorListener {
private final MediaItem mediaItem;
private final Handler handler;
public ComponentListener(MediaItem mediaItem, Looper looper) {
this.mediaItem = mediaItem;
handler = new Handler(looper);
}
@Override
public void onError(Exception e) {
TransformationException transformationException =
e instanceof PlaybackException
? TransformationException.createForPlaybackException((PlaybackException) e)
: TransformationException.createForUnexpected(e);
handleTransformationException(transformationException);
}
@Override
public void onEnded() {
handleTransformationEnded(/* exception= */ null);
}
@Override
public void onTransformationException(TransformationException exception) {
if (Looper.myLooper() == looper) {
handleTransformationException(exception);
} else {
handler.post(() -> handleTransformationException(exception));
}
}
private void handleTransformationException(TransformationException transformationException) {
if (isCancelling) {
// Resources are already being released.
listeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener -> listener.onTransformationError(mediaItem, transformationException));
listeners.flushEvents();
} else {
handleTransformationEnded(transformationException);
}
}
private void handleTransformationEnded(@Nullable TransformationException exception) {
MuxerWrapper muxerWrapper = Transformer.this.muxerWrapper;
@Nullable TransformationException resourceReleaseException = null;
try {
releaseResources(/* forCancellation= */ false);
} catch (TransformationException e) {
resourceReleaseException = e;
} catch (RuntimeException e) {
resourceReleaseException = TransformationException.createForUnexpected(e);
}
if (exception == null) {
// We only report the exception caused by releasing the resources if there is no other
// exception. It is more intuitive to call the error callback only once and reporting the
// exception caused by releasing the resources can be confusing if it is a consequence of
// the first exception.
exception = resourceReleaseException;
}
if (exception != null) {
TransformationException finalException = exception;
// TODO(b/213341814): Add event flags for Transformer events.
listeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener -> listener.onTransformationError(mediaItem, finalException));
} else {
TransformationResult result =
new TransformationResult.Builder()
.setDurationMs(checkNotNull(muxerWrapper).getDurationMs())
.setAverageAudioBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_AUDIO))
.setAverageVideoBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_VIDEO))
.setVideoFrameCount(muxerWrapper.getTrackSampleCount(C.TRACK_TYPE_VIDEO))
.setFileSizeBytes(getCurrentOutputFileCurrentSizeBytes())
.build();
listeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener -> listener.onTransformationCompleted(mediaItem, result));
}
listeners.flushEvents();
}
}
}