/*
* Copyright 2022 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.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.transformer.AssetLoader.SUPPORTED_OUTPUT_TYPE_DECODED;
import static androidx.media3.transformer.AssetLoader.SUPPORTED_OUTPUT_TYPE_ENCODED;
import static androidx.media3.transformer.ExportException.ERROR_CODE_FAILED_RUNTIME_CHECK;
import static androidx.media3.transformer.ExportException.ERROR_CODE_MUXING_FAILED;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_AVAILABLE;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NOT_STARTED;
import static androidx.media3.transformer.TransformerUtil.areVideoEffectsAllNoOp;
import static androidx.media3.transformer.TransformerUtil.containsSlowMotionData;
import static androidx.media3.transformer.TransformerUtil.getProcessedTrackType;
import static java.lang.Math.max;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.DebugViewProvider;
import androidx.media3.common.Effect;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.effect.Presentation;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* package */ final class TransformerInternal implements MuxerWrapper.Listener {
public interface Listener {
void onCompleted(ExportResult exportResult);
void onError(ExportResult exportResult, ExportException exportException);
}
/**
* Represents a reason for ending an export. May be one of {@link #END_REASON_COMPLETED}, {@link
* #END_REASON_CANCELLED} or {@link #END_REASON_ERROR}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({END_REASON_COMPLETED, END_REASON_CANCELLED, END_REASON_ERROR})
private @interface EndReason {}
/** The export completed successfully. */
private static final int END_REASON_COMPLETED = 0;
/** The export was cancelled. */
private static final int END_REASON_CANCELLED = 1;
/** An error occurred during the export. */
private static final int END_REASON_ERROR = 2;
// Internal messages.
private static final int MSG_START = 0;
private static final int MSG_REGISTER_SAMPLE_PIPELINE = 1;
private static final int MSG_DRAIN_PIPELINES = 2;
private static final int MSG_END = 3;
private static final int MSG_UPDATE_PROGRESS = 4;
private static final String TAG = "TransformerInternal";
private static final int DRAIN_PIPELINES_DELAY_MS = 10;
private final Context context;
private final Composition composition;
private final boolean compositionHasLoopingSequence;
private final CapturingEncoderFactory encoderFactory;
private final Listener listener;
private final HandlerWrapper applicationHandler;
private final Clock clock;
private final HandlerThread internalHandlerThread;
private final HandlerWrapper internalHandler;
private final List<SequenceAssetLoader> sequenceAssetLoaders;
private final AtomicInteger trackCountsToReport;
private final AtomicInteger tracksToAdd;
private final AtomicBoolean outputHasAudio;
private final AtomicBoolean outputHasVideo;
private final List<SamplePipeline> samplePipelines;
private final Object setMaxSequenceDurationUsLock;
private final MuxerWrapper muxerWrapper;
private final ConditionVariable transformerConditionVariable;
private final ExportResult.Builder exportResultBuilder;
private boolean isDrainingPipelines;
private long currentMaxSequenceDurationUs;
private int nonLoopingSequencesWithNonFinalDuration;
private @Transformer.ProgressState int progressState;
private @MonotonicNonNull RuntimeException cancelException;
private volatile boolean released;
// Warning suppression is needed to assign the MuxerWrapper with "this" as listener.
@SuppressWarnings("assignment.type.incompatible")
public TransformerInternal(
Context context,
Composition composition,
String outputPath,
TransformationRequest transformationRequest,
AssetLoader.Factory assetLoaderFactory,
Codec.EncoderFactory encoderFactory,
Muxer.Factory muxerFactory,
Listener listener,
FallbackListener fallbackListener,
HandlerWrapper applicationHandler,
DebugViewProvider debugViewProvider,
Clock clock) {
this.context = context;
this.composition = composition;
this.encoderFactory = new CapturingEncoderFactory(encoderFactory);
this.listener = listener;
this.applicationHandler = applicationHandler;
this.clock = clock;
internalHandlerThread = new HandlerThread("Transformer:Internal");
internalHandlerThread.start();
sequenceAssetLoaders = new ArrayList<>();
Looper internalLooper = internalHandlerThread.getLooper();
for (int i = 0; i < composition.sequences.size(); i++) {
SequenceAssetLoaderListener sequenceAssetLoaderListener =
new SequenceAssetLoaderListener(
/* sequenceIndex= */ i,
composition,
transformationRequest,
fallbackListener,
debugViewProvider);
EditedMediaItemSequence sequence = composition.sequences.get(i);
sequenceAssetLoaders.add(
new SequenceAssetLoader(
sequence,
composition.forceAudioTrack,
assetLoaderFactory,
internalLooper,
sequenceAssetLoaderListener,
clock));
if (!sequence.isLooping) {
// All sequences have a non-final duration at this point, as the AssetLoaders haven't
// started loading yet.
nonLoopingSequencesWithNonFinalDuration++;
}
}
compositionHasLoopingSequence =
nonLoopingSequencesWithNonFinalDuration != composition.sequences.size();
trackCountsToReport = new AtomicInteger(composition.sequences.size());
tracksToAdd = new AtomicInteger();
outputHasAudio = new AtomicBoolean();
outputHasVideo = new AtomicBoolean();
samplePipelines = new ArrayList<>();
setMaxSequenceDurationUsLock = new Object();
transformerConditionVariable = new ConditionVariable();
exportResultBuilder = new ExportResult.Builder();
// It's safe to use "this" because we don't send a message before exiting the constructor.
@SuppressWarnings("nullness:methodref.receiver.bound")
HandlerWrapper internalHandler =
clock.createHandler(internalLooper, /* callback= */ this::handleMessage);
this.internalHandler = internalHandler;
// It's safe to use "this" because we don't mux any data before exiting the constructor.
@SuppressWarnings("nullness:argument.type.incompatible")
MuxerWrapper muxerWrapper = new MuxerWrapper(outputPath, muxerFactory, /* listener= */ this);
this.muxerWrapper = muxerWrapper;
}
public void start() {
internalHandler.sendEmptyMessage(MSG_START);
}
public @Transformer.ProgressState int getProgress(ProgressHolder progressHolder) {
if (released) {
return PROGRESS_STATE_NOT_STARTED;
}
internalHandler.obtainMessage(MSG_UPDATE_PROGRESS, progressHolder).sendToTarget();
// TODO: figure out why calling clock.onThreadBlocked() here makes the tests fail.
transformerConditionVariable.blockUninterruptible();
transformerConditionVariable.close();
return progressState;
}
public void cancel() {
if (released) {
return;
}
internalHandler
.obtainMessage(MSG_END, END_REASON_CANCELLED, /* unused */ 0, /* exportException */ null)
.sendToTarget();
clock.onThreadBlocked();
transformerConditionVariable.blockUninterruptible();
transformerConditionVariable.close();
if (cancelException != null) {
throw cancelException;
}
}
// MuxerWrapper.Listener implementation
@Override
public void onTrackEnded(
@C.TrackType int trackType, Format format, int averageBitrate, int sampleCount) {
if (trackType == C.TRACK_TYPE_AUDIO) {
exportResultBuilder.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
.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);
internalHandler
.obtainMessage(MSG_END, END_REASON_COMPLETED, /* unused */ 0, /* exportException */ null)
.sendToTarget();
}
@Override
public void onError(ExportException exportException) {
internalHandler
.obtainMessage(MSG_END, END_REASON_ERROR, /* unused */ 0, exportException)
.sendToTarget();
}
// Private methods.
private boolean handleMessage(Message msg) {
// Some messages cannot be ignored when resources have been released. End messages must be
// handled to report release timeouts and to unblock the transformer condition variable in case
// of cancellation. Progress update messages must be handled to unblock the transformer
// condition variable.
if (released && msg.what != MSG_END && msg.what != MSG_UPDATE_PROGRESS) {
return true;
}
try {
switch (msg.what) {
case MSG_START:
startInternal();
break;
case MSG_REGISTER_SAMPLE_PIPELINE:
registerSamplePipelineInternal((SamplePipeline) msg.obj);
break;
case MSG_DRAIN_PIPELINES:
drainPipelinesInternal();
break;
case MSG_END:
endInternal(/* endReason= */ msg.arg1, /* exportException= */ (ExportException) msg.obj);
break;
case MSG_UPDATE_PROGRESS:
updateProgressInternal(/* progressHolder= */ (ProgressHolder) msg.obj);
break;
default:
return false;
}
} catch (ExportException e) {
endInternal(END_REASON_ERROR, e);
} catch (RuntimeException e) {
endInternal(END_REASON_ERROR, ExportException.createForUnexpected(e));
}
return true;
}
private void startInternal() {
for (int i = 0; i < sequenceAssetLoaders.size(); i++) {
sequenceAssetLoaders.get(i).start();
}
}
private void registerSamplePipelineInternal(SamplePipeline samplePipeline) {
samplePipelines.add(samplePipeline);
if (!isDrainingPipelines) {
internalHandler.sendEmptyMessage(MSG_DRAIN_PIPELINES);
isDrainingPipelines = true;
}
}
private void drainPipelinesInternal() throws ExportException {
for (int i = 0; i < samplePipelines.size(); i++) {
while (samplePipelines.get(i).processData()) {}
}
if (!muxerWrapper.isEnded()) {
internalHandler.sendEmptyMessageDelayed(MSG_DRAIN_PIPELINES, DRAIN_PIPELINES_DELAY_MS);
}
}
private void endInternal(@EndReason int endReason, @Nullable ExportException exportException) {
ImmutableList.Builder<ExportResult.ProcessedInput> processedInputsBuilder =
new ImmutableList.Builder<>();
for (int i = 0; i < sequenceAssetLoaders.size(); i++) {
processedInputsBuilder.addAll(sequenceAssetLoaders.get(i).getProcessedInputs());
}
exportResultBuilder
.setProcessedInputs(processedInputsBuilder.build())
.setAudioEncoderName(encoderFactory.getAudioEncoderName())
.setVideoEncoderName(encoderFactory.getVideoEncoderName());
boolean forCancellation = endReason == END_REASON_CANCELLED;
@Nullable ExportException releaseExportException = null;
boolean releasedPreviously = released;
if (!released) {
released = true;
// The video sample pipeline can hold buffers from the asset loader's decoder in a surface
// texture, so we release the video sample pipeline first to avoid releasing the codec while
// its buffers are pending processing.
for (int i = 0; i < samplePipelines.size(); i++) {
try {
samplePipelines.get(i).release();
} catch (RuntimeException e) {
if (releaseExportException == null) {
releaseExportException = ExportException.createForUnexpected(e);
// cancelException is not reported through a listener. It is thrown in cancel(), as this
// method is blocking.
cancelException = e;
}
}
}
for (int i = 0; i < sequenceAssetLoaders.size(); i++) {
try {
sequenceAssetLoaders.get(i).release();
} catch (RuntimeException e) {
if (releaseExportException == null) {
releaseExportException = ExportException.createForUnexpected(e);
cancelException = e;
}
}
}
try {
muxerWrapper.release(forCancellation);
} catch (Muxer.MuxerException e) {
if (releaseExportException == null) {
releaseExportException = ExportException.createForMuxer(e, ERROR_CODE_MUXING_FAILED);
}
} catch (RuntimeException e) {
if (releaseExportException == null) {
releaseExportException = ExportException.createForUnexpected(e);
cancelException = e;
}
}
// Quit thread lazily so that all events that got triggered when releasing the AssetLoader are
// still delivered.
internalHandler.post(internalHandlerThread::quitSafely);
}
if (forCancellation) {
transformerConditionVariable.open();
return;
}
ExportException exception = exportException;
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 = releaseExportException;
}
if (exception != null) {
if (releasedPreviously) {
Log.w(TAG, "Export error after export ended", exception);
return;
}
ExportException finalException = exception;
applicationHandler.post(
() ->
listener.onError(
exportResultBuilder.setExportException(finalException).build(), finalException));
} else {
if (releasedPreviously) {
return;
}
applicationHandler.post(() -> listener.onCompleted(exportResultBuilder.build()));
}
}
private void updateProgressInternal(ProgressHolder progressHolder) {
int progressSum = 0;
int progressCount = 0;
ProgressHolder individualProgressHolder = new ProgressHolder();
for (int i = 0; i < sequenceAssetLoaders.size(); i++) {
if (composition.sequences.get(i).isLooping) {
// Looping sequence progress is always unavailable. Skip it.
continue;
}
progressState = sequenceAssetLoaders.get(i).getProgress(individualProgressHolder);
if (progressState != PROGRESS_STATE_AVAILABLE) {
transformerConditionVariable.open();
return;
}
progressSum += individualProgressHolder.progress;
progressCount++;
}
progressHolder.progress = progressSum / progressCount;
transformerConditionVariable.open();
}
private final class SequenceAssetLoaderListener implements AssetLoader.Listener {
private final int sequenceIndex;
private final ImmutableList<EditedMediaItem> editedMediaItems;
private final Composition composition;
private final TransformationRequest transformationRequest;
private final FallbackListener fallbackListener;
private final DebugViewProvider debugViewProvider;
private final Map<Integer, AddedTrackInfo> addedTrackInfoByTrackType;
private long currentSequenceDurationUs;
public SequenceAssetLoaderListener(
int sequenceIndex,
Composition composition,
TransformationRequest transformationRequest,
FallbackListener fallbackListener,
DebugViewProvider debugViewProvider) {
this.sequenceIndex = sequenceIndex;
editedMediaItems = composition.sequences.get(sequenceIndex).editedMediaItems;
this.composition = composition;
this.transformationRequest = transformationRequest;
this.fallbackListener = fallbackListener;
this.debugViewProvider = debugViewProvider;
addedTrackInfoByTrackType = new HashMap<>();
}
@Override
public void onDurationUs(long durationUs) {}
@Override
public void onTrackCount(int trackCount) {
if (trackCount <= 0) {
onError(
ExportException.createForAssetLoader(
new IllegalStateException("AssetLoader instances must provide at least 1 track."),
ERROR_CODE_FAILED_RUNTIME_CHECK));
return;
}
tracksToAdd.addAndGet(trackCount);
trackCountsToReport.decrementAndGet();
}
@Override
public boolean onTrackAdded(
Format firstAssetLoaderInputFormat,
@AssetLoader.SupportedOutputTypes int supportedOutputTypes) {
@C.TrackType
int trackType = getProcessedTrackType(firstAssetLoaderInputFormat.sampleMimeType);
AddedTrackInfo trackInfo =
new AddedTrackInfo(firstAssetLoaderInputFormat, supportedOutputTypes);
addedTrackInfoByTrackType.put(trackType, trackInfo);
if (trackType == C.TRACK_TYPE_AUDIO) {
outputHasAudio.set(true);
} else {
outputHasVideo.set(true);
}
if (tracksToAdd.decrementAndGet() == 0 && trackCountsToReport.get() == 0) {
int outputTrackCount = (outputHasAudio.get() ? 1 : 0) + (outputHasVideo.get() ? 1 : 0);
muxerWrapper.setTrackCount(outputTrackCount);
fallbackListener.setTrackCount(outputTrackCount);
}
return trackInfo.shouldTranscode;
}
@Nullable
@Override
public SampleConsumer onOutputFormat(Format assetLoaderOutputFormat) throws ExportException {
if (trackCountsToReport.get() > 0 || tracksToAdd.get() > 0) {
return null;
}
@C.TrackType int trackType = getProcessedTrackType(assetLoaderOutputFormat.sampleMimeType);
AddedTrackInfo trackInfo = checkStateNotNull(addedTrackInfoByTrackType.get(trackType));
SamplePipeline samplePipeline = getSamplePipeline(assetLoaderOutputFormat, trackInfo);
OnMediaItemChangedListener onMediaItemChangedListener =
(editedMediaItem, durationUs, trackFormat, isLast) -> {
onMediaItemChanged(trackType, durationUs, isLast);
samplePipeline.onMediaItemChanged(editedMediaItem, durationUs, trackFormat, isLast);
};
sequenceAssetLoaders
.get(sequenceIndex)
.addOnMediaItemChangedListener(onMediaItemChangedListener, trackType);
internalHandler.obtainMessage(MSG_REGISTER_SAMPLE_PIPELINE, samplePipeline).sendToTarget();
return samplePipeline;
}
@Override
public void onError(ExportException exportException) {
TransformerInternal.this.onError(exportException);
}
// Private methods.
private SamplePipeline getSamplePipeline(
Format firstAssetLoaderOutputFormat, AddedTrackInfo addedTrackInfo) throws ExportException {
if (addedTrackInfo.shouldTranscode) {
EditedMediaItem firstEditedMediaItem = editedMediaItems.get(0);
if (MimeTypes.isAudio(firstAssetLoaderOutputFormat.sampleMimeType)) {
return new AudioSamplePipeline(
addedTrackInfo.firstAssetLoaderInputFormat,
/* firstPipelineInputFormat= */ firstAssetLoaderOutputFormat,
transformationRequest,
firstEditedMediaItem.flattenForSlowMotion,
firstEditedMediaItem.effects.audioProcessors,
encoderFactory,
muxerWrapper,
fallbackListener);
} else { // MIME type is video or image.
ImmutableList<Effect> compositionVideoEffects = composition.effects.videoEffects;
@Nullable
Presentation compositionPresentation =
compositionVideoEffects.isEmpty()
? null
: (Presentation) compositionVideoEffects.get(0);
// TODO(b/267301878): Pass firstAssetLoaderOutputFormat once surface creation not in VSP.
return new VideoSamplePipeline(
context,
addedTrackInfo.firstAssetLoaderInputFormat,
transformationRequest,
firstEditedMediaItem.effects.videoEffects,
compositionPresentation,
firstEditedMediaItem.effects.videoFrameProcessorFactory,
encoderFactory,
muxerWrapper,
/* errorConsumer= */ this::onError,
fallbackListener,
debugViewProvider);
}
}
return new EncodedSamplePipeline(
firstAssetLoaderOutputFormat, transformationRequest, muxerWrapper, fallbackListener);
}
/**
* Updates the maximum sequence duration and passes it to the SequenceAssetLoaders if needed.
*/
private void onMediaItemChanged(@C.TrackType int trackType, long durationUs, boolean isLast) {
if (!compositionHasLoopingSequence) {
// The code in this method handles looping sequences. Skip it if there are none.
return;
}
if (addedTrackInfoByTrackType.size() > 1 && trackType == C.TRACK_TYPE_VIDEO) {
// Make sure this method is only executed once per MediaItem (and not per track).
return;
}
if (composition.sequences.get(sequenceIndex).isLooping) {
return;
}
checkState(
durationUs != C.TIME_UNSET,
"MediaItem duration required for sequence looping could not be extracted.");
currentSequenceDurationUs += durationUs;
// onMediaItemChanged can be executed concurrently from different sequences.
synchronized (setMaxSequenceDurationUsLock) {
if (isLast) {
// The total sequence duration is known when the last MediaItem is loaded.
nonLoopingSequencesWithNonFinalDuration--;
}
boolean isMaxSequenceDurationUsFinal = nonLoopingSequencesWithNonFinalDuration == 0;
if (currentSequenceDurationUs > currentMaxSequenceDurationUs
|| isMaxSequenceDurationUsFinal) {
currentMaxSequenceDurationUs =
max(currentSequenceDurationUs, currentMaxSequenceDurationUs);
for (int i = 0; i < sequenceAssetLoaders.size(); i++) {
sequenceAssetLoaders
.get(i)
.setMaxSequenceDurationUs(
currentMaxSequenceDurationUs, isMaxSequenceDurationUsFinal);
}
}
}
}
private final class AddedTrackInfo {
public final Format firstAssetLoaderInputFormat;
public final boolean shouldTranscode;
public AddedTrackInfo(
Format firstAssetLoaderInputFormat,
@AssetLoader.SupportedOutputTypes int supportedOutputTypes) {
this.firstAssetLoaderInputFormat = firstAssetLoaderInputFormat;
shouldTranscode = shouldTranscode(firstAssetLoaderInputFormat, supportedOutputTypes);
}
private boolean shouldTranscode(
Format inputFormat, @AssetLoader.SupportedOutputTypes int supportedOutputTypes) {
boolean assetLoaderCanOutputDecoded =
(supportedOutputTypes & SUPPORTED_OUTPUT_TYPE_DECODED) != 0;
boolean assetLoaderCanOutputEncoded =
(supportedOutputTypes & SUPPORTED_OUTPUT_TYPE_ENCODED) != 0;
checkArgument(assetLoaderCanOutputDecoded || assetLoaderCanOutputEncoded);
@C.TrackType int trackType = getProcessedTrackType(inputFormat.sampleMimeType);
boolean shouldTranscode = false;
if (!assetLoaderCanOutputEncoded) {
shouldTranscode = true;
} else if (trackType == C.TRACK_TYPE_AUDIO) {
shouldTranscode = shouldTranscodeAudio(inputFormat);
} else if (trackType == C.TRACK_TYPE_VIDEO) {
shouldTranscode = shouldTranscodeVideo(inputFormat);
}
checkState(!shouldTranscode || assetLoaderCanOutputDecoded);
return shouldTranscode;
}
private boolean shouldTranscodeAudio(Format inputFormat) {
if (composition.sequences.size() > 1 || editedMediaItems.size() > 1) {
return !composition.transmuxAudio;
}
if (encoderFactory.audioNeedsEncoding()) {
return true;
}
if (transformationRequest.audioMimeType != null
&& !transformationRequest.audioMimeType.equals(inputFormat.sampleMimeType)) {
return true;
}
if (transformationRequest.audioMimeType == null
&& !muxerWrapper.supportsSampleMimeType(inputFormat.sampleMimeType)) {
return true;
}
EditedMediaItem firstEditedMediaItem = editedMediaItems.get(0);
if (firstEditedMediaItem.flattenForSlowMotion && containsSlowMotionData(inputFormat)) {
return true;
}
if (!firstEditedMediaItem.effects.audioProcessors.isEmpty()) {
return true;
}
return false;
}
private boolean shouldTranscodeVideo(Format inputFormat) {
if (composition.sequences.size() > 1 || editedMediaItems.size() > 1) {
return !composition.transmuxVideo;
}
EditedMediaItem firstEditedMediaItem = editedMediaItems.get(0);
if (firstEditedMediaItem.mediaItem.clippingConfiguration.startPositionMs > 0
&& !firstEditedMediaItem.mediaItem.clippingConfiguration.startsAtKeyFrame) {
return true;
}
if (encoderFactory.videoNeedsEncoding()) {
return true;
}
if (transformationRequest.hdrMode != TransformationRequest.HDR_MODE_KEEP_HDR) {
return true;
}
if (transformationRequest.videoMimeType != null
&& !transformationRequest.videoMimeType.equals(inputFormat.sampleMimeType)) {
return true;
}
if (transformationRequest.videoMimeType == null
&& !muxerWrapper.supportsSampleMimeType(inputFormat.sampleMimeType)) {
return true;
}
if (inputFormat.pixelWidthHeightRatio != 1f) {
return true;
}
if (!areVideoEffectsAllNoOp(firstEditedMediaItem.effects.videoEffects, inputFormat)) {
return true;
}
return false;
}
}
}
}