diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java index 90771f496f..03d5f5ce55 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java @@ -23,14 +23,13 @@ import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Util.contains; 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.Composition.HDR_MODE_KEEP_HDR; 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 androidx.media3.transformer.TransformerUtil.shouldTranscodeAudio; +import static androidx.media3.transformer.TransformerUtil.shouldTranscodeVideo; import static java.lang.Math.max; import static java.lang.annotation.ElementType.TYPE_USE; @@ -45,14 +44,12 @@ 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.VideoFrameProcessor; import androidx.media3.common.util.Clock; import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.HandlerWrapper; -import androidx.media3.effect.ScaleAndRotateTransformation; import com.google.common.collect.ImmutableList; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -445,7 +442,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final class SequenceAssetLoaderListener implements AssetLoader.Listener { private final int sequenceIndex; - private final ImmutableList editedMediaItems; + private final EditedMediaItem firstEditedMediaItem; private final Composition composition; private final TransformationRequest transformationRequest; private final AudioMixer.Factory audioMixerFactory; @@ -463,7 +460,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; FallbackListener fallbackListener, DebugViewProvider debugViewProvider) { this.sequenceIndex = sequenceIndex; - this.editedMediaItems = composition.sequences.get(sequenceIndex).editedMediaItems; + this.firstEditedMediaItem = composition.sequences.get(sequenceIndex).editedMediaItems.get(0); this.composition = composition; this.transformationRequest = transformationRequest; this.audioMixerFactory = audioMixerFactory; @@ -535,7 +532,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } GraphInput sampleExporterInput = - sampleExporter.getInput(editedMediaItems.get(0), assetLoaderOutputFormat); + sampleExporter.getInput(firstEditedMediaItem, assetLoaderOutputFormat); OnMediaItemChangedListener onMediaItemChangedListener = (editedMediaItem, durationUs, trackFormat, isLast) -> { onMediaItemChanged(trackType, durationUs, isLast); @@ -581,7 +578,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; firstAssetLoaderInputFormat, /* firstInputFormat= */ assetLoaderOutputFormat, transformationRequest, - editedMediaItems.get(0), + firstEditedMediaItem, audioMixerFactory, encoderFactory, muxerWrapper, @@ -680,95 +677,29 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (!assetLoaderCanOutputEncoded) { shouldTranscode = true; } else if (trackType == TRACK_TYPE_AUDIO) { - shouldTranscode = shouldTranscodeAudio(inputFormat); + shouldTranscode = + shouldTranscodeAudio( + inputFormat, + composition, + sequenceIndex, + transformationRequest, + encoderFactory, + muxerWrapper); } else if (trackType == C.TRACK_TYPE_VIDEO) { - shouldTranscode = shouldTranscodeVideo(inputFormat); + shouldTranscode = + shouldTranscodeVideo( + inputFormat, + composition, + sequenceIndex, + transformationRequest, + encoderFactory, + muxerWrapper); } 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 != 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; - } - ImmutableList videoEffects = firstEditedMediaItem.effects.videoEffects; - return !videoEffects.isEmpty() - && !areVideoEffectsAllNoOp(videoEffects, inputFormat) - && !hasOnlyRegularRotationEffect(videoEffects); - } - - private boolean hasOnlyRegularRotationEffect(ImmutableList videoEffects) { - if (videoEffects.size() != 1) { - return false; - } - Effect videoEffect = videoEffects.get(0); - if (!(videoEffect instanceof ScaleAndRotateTransformation)) { - return false; - } - ScaleAndRotateTransformation scaleAndRotateTransformation = - (ScaleAndRotateTransformation) videoEffect; - if (scaleAndRotateTransformation.scaleX != 1f || scaleAndRotateTransformation.scaleY != 1f) { - return false; - } - float rotationDegrees = scaleAndRotateTransformation.rotationDegrees; - if (rotationDegrees == 90f || rotationDegrees == 180f || rotationDegrees == 270f) { - // The MuxerWrapper rotation is clockwise while the ScaleAndRotateTransformation rotation - // is counterclockwise. - muxerWrapper.setAdditionalRotationDegrees(360 - Math.round(rotationDegrees)); - return true; - } - return false; - } } /** Tracks the inputs and outputs of {@link AssetLoader AssetLoaders}. */ diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerUtil.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerUtil.java index 0431f83153..db065f5960 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerUtil.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerUtil.java @@ -16,6 +16,8 @@ package androidx.media3.transformer; +import static androidx.media3.transformer.Composition.HDR_MODE_KEEP_HDR; + import android.media.MediaCodec; import androidx.annotation.Nullable; import androidx.media3.common.C; @@ -24,6 +26,7 @@ import androidx.media3.common.Format; import androidx.media3.common.Metadata; import androidx.media3.common.MimeTypes; import androidx.media3.effect.GlEffect; +import androidx.media3.effect.ScaleAndRotateTransformation; import androidx.media3.extractor.metadata.mp4.SlowMotionData; import com.google.common.collect.ImmutableList; @@ -45,6 +48,110 @@ import com.google.common.collect.ImmutableList; return trackType == C.TRACK_TYPE_IMAGE ? C.TRACK_TYPE_VIDEO : trackType; } + /** Returns {@link MediaCodec} flags corresponding to {@link C.BufferFlags}. */ + public static int getMediaCodecFlags(@C.BufferFlags int flags) { + int mediaCodecFlags = 0; + if ((flags & C.BUFFER_FLAG_KEY_FRAME) == C.BUFFER_FLAG_KEY_FRAME) { + mediaCodecFlags |= MediaCodec.BUFFER_FLAG_KEY_FRAME; + } + if ((flags & C.BUFFER_FLAG_END_OF_STREAM) == C.BUFFER_FLAG_END_OF_STREAM) { + mediaCodecFlags |= MediaCodec.BUFFER_FLAG_END_OF_STREAM; + } + return mediaCodecFlags; + } + + /** Returns whether the audio track should be transcoded. */ + public static boolean shouldTranscodeAudio( + Format inputFormat, + Composition composition, + int sequenceIndex, + TransformationRequest transformationRequest, + Codec.EncoderFactory encoderFactory, + MuxerWrapper muxerWrapper) { + if (composition.sequences.size() > 1 + || composition.sequences.get(sequenceIndex).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 = + composition.sequences.get(sequenceIndex).editedMediaItems.get(0); + if (firstEditedMediaItem.flattenForSlowMotion && containsSlowMotionData(inputFormat)) { + return true; + } + if (!firstEditedMediaItem.effects.audioProcessors.isEmpty()) { + return true; + } + return false; + } + + /** + * Returns whether the {@link Format} contains {@linkplain SlowMotionData slow motion metadata}. + */ + private static boolean containsSlowMotionData(Format format) { + @Nullable Metadata metadata = format.metadata; + if (metadata == null) { + return false; + } + for (int i = 0; i < metadata.length(); i++) { + if (metadata.get(i) instanceof SlowMotionData) { + return true; + } + } + return false; + } + + /** Returns whether the video track should be transcoded. */ + public static boolean shouldTranscodeVideo( + Format inputFormat, + Composition composition, + int sequenceIndex, + TransformationRequest transformationRequest, + Codec.EncoderFactory encoderFactory, + MuxerWrapper muxerWrapper) { + + if (composition.sequences.size() > 1 + || composition.sequences.get(sequenceIndex).editedMediaItems.size() > 1) { + return !composition.transmuxVideo; + } + EditedMediaItem firstEditedMediaItem = + composition.sequences.get(sequenceIndex).editedMediaItems.get(0); + if (firstEditedMediaItem.mediaItem.clippingConfiguration.startPositionMs > 0 + && !firstEditedMediaItem.mediaItem.clippingConfiguration.startsAtKeyFrame) { + return true; + } + if (encoderFactory.videoNeedsEncoding()) { + return true; + } + if (transformationRequest.hdrMode != 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; + } + ImmutableList videoEffects = firstEditedMediaItem.effects.videoEffects; + return !videoEffects.isEmpty() + && !areVideoEffectsAllNoOp(videoEffects, inputFormat) + && !hasOnlyRegularRotationEffect(videoEffects, muxerWrapper); + } + /** * Returns whether the collection of {@code videoEffects} would be a {@linkplain * GlEffect#isNoOp(int, int) no-op}, if queued samples of this {@link Format}. @@ -70,31 +177,27 @@ import com.google.common.collect.ImmutableList; return true; } - /** - * Returns whether the {@link Format} contains {@linkplain SlowMotionData slow motion metadata}. - */ - public static boolean containsSlowMotionData(Format format) { - @Nullable Metadata metadata = format.metadata; - if (metadata == null) { + private static boolean hasOnlyRegularRotationEffect( + ImmutableList videoEffects, MuxerWrapper muxerWrapper) { + if (videoEffects.size() != 1) { return false; } - for (int i = 0; i < metadata.length(); i++) { - if (metadata.get(i) instanceof SlowMotionData) { - return true; - } + Effect videoEffect = videoEffects.get(0); + if (!(videoEffect instanceof ScaleAndRotateTransformation)) { + return false; + } + ScaleAndRotateTransformation scaleAndRotateTransformation = + (ScaleAndRotateTransformation) videoEffect; + if (scaleAndRotateTransformation.scaleX != 1f || scaleAndRotateTransformation.scaleY != 1f) { + return false; + } + float rotationDegrees = scaleAndRotateTransformation.rotationDegrees; + if (rotationDegrees == 90f || rotationDegrees == 180f || rotationDegrees == 270f) { + // The MuxerWrapper rotation is clockwise while the ScaleAndRotateTransformation rotation + // is counterclockwise. + muxerWrapper.setAdditionalRotationDegrees(360 - Math.round(rotationDegrees)); + return true; } return false; } - - /** Returns {@link MediaCodec} flags corresponding to {@link C.BufferFlags}. */ - public static int getMediaCodecFlags(@C.BufferFlags int flags) { - int mediaCodecFlags = 0; - if ((flags & C.BUFFER_FLAG_KEY_FRAME) == C.BUFFER_FLAG_KEY_FRAME) { - mediaCodecFlags |= MediaCodec.BUFFER_FLAG_KEY_FRAME; - } - if ((flags & C.BUFFER_FLAG_END_OF_STREAM) == C.BUFFER_FLAG_END_OF_STREAM) { - mediaCodecFlags |= MediaCodec.BUFFER_FLAG_END_OF_STREAM; - } - return mediaCodecFlags; - } }