diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java index e2df0f4fd5..7e7a730b0f 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java @@ -272,8 +272,6 @@ public final class TransformerActivity extends AppCompatActivity { Transformer.Builder transformerBuilder = new Transformer.Builder(/* context= */ this); if (bundle != null) { TransformationRequest.Builder requestBuilder = new TransformationRequest.Builder(); - requestBuilder.setFlattenForSlowMotion( - bundle.getBoolean(ConfigurationActivity.SHOULD_FLATTEN_FOR_SLOW_MOTION)); @Nullable String audioMimeType = bundle.getString(ConfigurationActivity.AUDIO_MIME_TYPE); if (audioMimeType != null) { requestBuilder.setAudioMimeType(audioMimeType); @@ -352,6 +350,8 @@ public final class TransformerActivity extends AppCompatActivity { return editedMediaItemBuilder .setRemoveAudio(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_AUDIO)) .setRemoveVideo(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_VIDEO)) + .setFlattenForSlowMotion( + bundle.getBoolean(ConfigurationActivity.SHOULD_FLATTEN_FOR_SLOW_MOTION)) .setEffects(new Effects(audioProcessors, videoEffects)) .build(); } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java index e9e98553e8..d574c5775c 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java @@ -35,7 +35,6 @@ import androidx.media3.transformer.AndroidTestUtil.ForceEncodeEncoderFactory; import androidx.media3.transformer.DefaultEncoderFactory; import androidx.media3.transformer.EditedMediaItem; import androidx.media3.transformer.Effects; -import androidx.media3.transformer.TransformationRequest; import androidx.media3.transformer.Transformer; import androidx.media3.transformer.TransformerAndroidTestRunner; import androidx.media3.transformer.VideoEncoderSettings; @@ -203,13 +202,11 @@ public class TransformationTest { return; } - Transformer transformer = - new Transformer.Builder(context) - .setTransformationRequest( - new TransformationRequest.Builder().setFlattenForSlowMotion(true).build()) - .build(); + Transformer transformer = new Transformer.Builder(context).build(); EditedMediaItem editedMediaItem = - new EditedMediaItem.Builder(MediaItem.fromUri(Uri.parse(MP4_ASSET_SEF_URI_STRING))).build(); + new EditedMediaItem.Builder(MediaItem.fromUri(Uri.parse(MP4_ASSET_SEF_URI_STRING))) + .setFlattenForSlowMotion(true) + .build(); new TransformerAndroidTestRunner.Builder(context, transformer) .build() .run(testId, editedMediaItem); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AssetLoader.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AssetLoader.java index 535dd37444..8ee4b990e4 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/AssetLoader.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AssetLoader.java @@ -76,7 +76,7 @@ public interface AssetLoader { * this is done on decoded samples. * *

For more information on slow motion flattening, see {@link - * TransformationRequest.Builder#setFlattenForSlowMotion(boolean)}. + * EditedMediaItem.Builder#setFlattenForSlowMotion(boolean)}. */ @CanIgnoreReturnValue Factory setFlattenVideoForSlowMotion(boolean flattenVideoForSlowMotion); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AudioSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AudioSamplePipeline.java index 2e463c4c01..f98bef791d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/AudioSamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AudioSamplePipeline.java @@ -63,6 +63,7 @@ import org.checkerframework.dataflow.qual.Pure; long streamStartPositionUs, long streamOffsetUs, TransformationRequest transformationRequest, + boolean flattenForSlowMotion, ImmutableList audioProcessors, long generateSilentAudioDurationUs, Codec.EncoderFactory encoderFactory, @@ -89,7 +90,7 @@ import org.checkerframework.dataflow.qual.Pure; encoderInputBuffer = new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED); encoderOutputBuffer = new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED); - if (transformationRequest.flattenForSlowMotion) { + if (flattenForSlowMotion) { audioProcessors = new ImmutableList.Builder() .add(new SpeedChangingAudioProcessor(new SegmentSpeedProvider(inputFormat))) diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultAssetLoaderFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultAssetLoaderFactory.java index 9e0e30e633..ce7cf92613 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultAssetLoaderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultAssetLoaderFactory.java @@ -34,20 +34,34 @@ public final class DefaultAssetLoaderFactory implements AssetLoader.Factory { * Creates an instance. * * @param context The {@link Context}. - * @param mediaSourceFactory The {@link MediaSource.Factory} to use to retrieve the samples to - * transform when an {@link ExoPlayerAssetLoader} is used. * @param decoderFactory The {@link Codec.DecoderFactory} to use to decode the samples (if * necessary). * @param clock The {@link Clock} to use. It should always be {@link Clock#DEFAULT}, except for * testing. */ + public DefaultAssetLoaderFactory( + Context context, Codec.DecoderFactory decoderFactory, Clock clock) { + assetLoaderFactory = new ExoPlayerAssetLoader.Factory(context, decoderFactory, clock); + } + + /** + * Creates an instance. + * + * @param context The {@link Context}. + * @param decoderFactory The {@link Codec.DecoderFactory} to use to decode the samples (if + * necessary). + * @param clock The {@link Clock} to use. It should always be {@link Clock#DEFAULT}, except for + * testing. + * @param mediaSourceFactory The {@link MediaSource.Factory} to use to retrieve the samples to + * transform when an {@link ExoPlayerAssetLoader} is used. + */ public DefaultAssetLoaderFactory( Context context, - MediaSource.Factory mediaSourceFactory, Codec.DecoderFactory decoderFactory, - Clock clock) { + Clock clock, + MediaSource.Factory mediaSourceFactory) { assetLoaderFactory = - new ExoPlayerAssetLoader.Factory(context, mediaSourceFactory, decoderFactory, clock); + new ExoPlayerAssetLoader.Factory(context, decoderFactory, clock, mediaSourceFactory); } @Override diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/EditedMediaItem.java b/libraries/transformer/src/main/java/androidx/media3/transformer/EditedMediaItem.java index b85ffd4a31..9720501905 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/EditedMediaItem.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/EditedMediaItem.java @@ -15,10 +15,13 @@ */ package androidx.media3.transformer; +import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkState; import androidx.media3.common.MediaItem; import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.source.MediaSource; +import androidx.media3.extractor.mp4.Mp4Extractor; import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CanIgnoreReturnValue; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -34,6 +37,7 @@ public class EditedMediaItem { private boolean removeAudio; private boolean removeVideo; + private boolean flattenForSlowMotion; private @MonotonicNonNull Effects effects; /** @@ -79,6 +83,47 @@ public class EditedMediaItem { return this; } + /** + * Sets whether to flatten the {@link MediaItem} if it contains slow motion markers. + * + *

The default value is {@code false}. + * + *

The flattened output is obtained by removing the slow motion metadata and by actually + * slowing down the parts of the video and audio streams defined in this metadata. + * + *

Only Samsung Extension Format (SEF) slow motion metadata type is supported. Flattening has + * no effect if the input does not contain this metadata type. + * + *

For SEF slow motion media, the following assumptions are made on the input: + * + *

+ * + *

If using an {@link ExoPlayerAssetLoader.Factory} with a provided {@link + * MediaSource.Factory}, make sure that {@link Mp4Extractor#FLAG_READ_SEF_DATA} is set on the + * {@link Mp4Extractor} used. Otherwise, the slow motion metadata will be ignored and the input + * won't be flattened. + * + *

Using slow motion flattening together with {@link MediaItem.ClippingConfiguration} is not + * supported yet. + * + * @param flattenForSlowMotion Whether to flatten for slow motion. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setFlattenForSlowMotion(boolean flattenForSlowMotion) { + // TODO(b/233986762): Support clipping with SEF flattening. + checkArgument( + mediaItem.clippingConfiguration.equals(MediaItem.ClippingConfiguration.UNSET) + || !flattenForSlowMotion, + "Slow motion flattening is not supported when clipping is requested"); + this.flattenForSlowMotion = flattenForSlowMotion; + return this; + } + /** * Sets the {@link Effects} to apply to the {@link MediaItem}. * @@ -100,21 +145,28 @@ public class EditedMediaItem { new Effects( /* audioProcessors= */ ImmutableList.of(), /* videoEffects= */ ImmutableList.of()); } - return new EditedMediaItem(mediaItem, removeAudio, removeVideo, effects); + return new EditedMediaItem( + mediaItem, removeAudio, removeVideo, flattenForSlowMotion, effects); } } /* package */ final MediaItem mediaItem; /* package */ final boolean removeAudio; /* package */ final boolean removeVideo; + /* package */ final boolean flattenForSlowMotion; /* package */ final Effects effects; private EditedMediaItem( - MediaItem mediaItem, boolean removeAudio, boolean removeVideo, Effects effects) { + MediaItem mediaItem, + boolean removeAudio, + boolean removeVideo, + boolean flattenForSlowMotion, + Effects effects) { checkState(!removeAudio || !removeVideo, "Audio and video cannot both be removed"); this.mediaItem = mediaItem; this.removeAudio = removeAudio; this.removeVideo = removeVideo; + this.flattenForSlowMotion = flattenForSlowMotion; this.effects = effects; } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Effects.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Effects.java index c6b2348753..5fcb0f63ef 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Effects.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Effects.java @@ -49,8 +49,7 @@ public final class Effects { * They are applied in the order of the list, and buffers will only be modified by that {@link * AudioProcessor} if it {@link AudioProcessor#isActive()} based on the current configuration. * @param videoEffects The list of {@link Effect} instances to apply to each video frame. They are - * applied in the order of the list, after {@linkplain - * TransformationRequest.Builder#setFlattenForSlowMotion(boolean) slow-motion flattening}. + * applied in the order of the list. * @param frameProcessorFactory The {@link FrameProcessor.Factory} for the {@link FrameProcessor} * to use when applying the {@code videoEffects} to the video frames. */ diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ExoPlayerAssetLoader.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ExoPlayerAssetLoader.java index 85e3a425c0..91990daf44 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ExoPlayerAssetLoader.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ExoPlayerAssetLoader.java @@ -47,10 +47,13 @@ import androidx.media3.exoplayer.Renderer; import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.audio.AudioRendererEventListener; import androidx.media3.exoplayer.metadata.MetadataOutput; +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.text.TextOutput; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.video.VideoRendererEventListener; +import androidx.media3.extractor.DefaultExtractorsFactory; +import androidx.media3.extractor.mp4.Mp4Extractor; import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -62,34 +65,50 @@ public final class ExoPlayerAssetLoader implements AssetLoader { public static final class Factory implements AssetLoader.Factory { private final Context context; - private final MediaSource.Factory mediaSourceFactory; private final Codec.DecoderFactory decoderFactory; private final Clock clock; + @Nullable private final MediaSource.Factory mediaSourceFactory; private boolean removeAudio; private boolean removeVideo; private boolean flattenVideoForSlowMotion; /** - * Creates an instance. + * Creates an instance using a {@link DefaultMediaSourceFactory}. * * @param context The {@link Context}. - * @param mediaSourceFactory The {@link MediaSource.Factory} to use to retrieve the samples to - * transform. * @param decoderFactory The {@link Codec.DecoderFactory} to use to decode the samples (if * necessary). * @param clock The {@link Clock} to use. It should always be {@link Clock#DEFAULT}, except for * testing. */ - public Factory( - Context context, - MediaSource.Factory mediaSourceFactory, - Codec.DecoderFactory decoderFactory, - Clock clock) { + public Factory(Context context, Codec.DecoderFactory decoderFactory, Clock clock) { this.context = context; - this.mediaSourceFactory = mediaSourceFactory; this.decoderFactory = decoderFactory; this.clock = clock; + this.mediaSourceFactory = null; + } + + /** + * Creates an instance. + * + * @param context The {@link Context}. + * @param decoderFactory The {@link Codec.DecoderFactory} to use to decode the samples (if + * necessary). + * @param clock The {@link Clock} to use. It should always be {@link Clock#DEFAULT}, except for + * testing. + * @param mediaSourceFactory The {@link MediaSource.Factory} to use to retrieve the samples to + * transform. + */ + public Factory( + Context context, + Codec.DecoderFactory decoderFactory, + Clock clock, + MediaSource.Factory mediaSourceFactory) { + this.context = context; + this.decoderFactory = decoderFactory; + this.clock = clock; + this.mediaSourceFactory = mediaSourceFactory; } @Override @@ -115,6 +134,14 @@ public final class ExoPlayerAssetLoader implements AssetLoader { @Override public AssetLoader createAssetLoader(MediaItem mediaItem, Looper looper, Listener listener) { + MediaSource.Factory mediaSourceFactory = this.mediaSourceFactory; + if (mediaSourceFactory == null) { + DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); + if (flattenVideoForSlowMotion) { + defaultExtractorsFactory.setMp4ExtractorFlags(Mp4Extractor.FLAG_READ_SEF_DATA); + } + mediaSourceFactory = new DefaultMediaSourceFactory(context, defaultExtractorsFactory); + } return new ExoPlayerAssetLoader( context, mediaItem, diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java index dc81be9107..e03a9d199c 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java @@ -26,8 +26,6 @@ import androidx.media3.common.C; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; -import androidx.media3.exoplayer.source.MediaSource; -import androidx.media3.extractor.mp4.Mp4Extractor; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -110,7 +108,6 @@ public final class TransformationRequest { /** A builder for {@link TransformationRequest} instances. */ public static final class Builder { - private boolean flattenForSlowMotion; private int outputHeight; @Nullable private String audioMimeType; @Nullable private String videoMimeType; @@ -127,48 +124,12 @@ public final class TransformationRequest { } private Builder(TransformationRequest transformationRequest) { - this.flattenForSlowMotion = transformationRequest.flattenForSlowMotion; this.outputHeight = transformationRequest.outputHeight; this.audioMimeType = transformationRequest.audioMimeType; this.videoMimeType = transformationRequest.videoMimeType; this.hdrMode = transformationRequest.hdrMode; } - /** - * Sets whether the input should be flattened for media containing slow motion markers. - * - *

The transformed output is obtained by removing the slow motion metadata and by actually - * slowing down the parts of the video and audio streams defined in this metadata. The default - * value for {@code flattenForSlowMotion} is {@code false}. - * - *

Only Samsung Extension Format (SEF) slow motion metadata type is supported. The - * transformation has no effect if the input does not contain this metadata type. - * - *

For SEF slow motion media, the following assumptions are made on the input: - * - *

- * - *

If using an {@link ExoPlayerAssetLoader.Factory} with a provided {@link - * MediaSource.Factory}, make sure that {@link Mp4Extractor#FLAG_READ_SEF_DATA} is set on the - * {@link Mp4Extractor} used. Otherwise, the slow motion metadata will be ignored and the input - * won't be flattened. - * - *

Using slow motion flattening together with {@link - * androidx.media3.common.MediaItem.ClippingConfiguration} is not supported yet. - * - * @param flattenForSlowMotion Whether to flatten for slow motion. - * @return This builder. - */ - @CanIgnoreReturnValue - public Builder setFlattenForSlowMotion(boolean flattenForSlowMotion) { - this.flattenForSlowMotion = flattenForSlowMotion; - return this; - } - /** * Sets the output resolution using the output height of the displayed video. * @@ -290,17 +251,10 @@ public final class TransformationRequest { /** Builds a {@link TransformationRequest} instance. */ public TransformationRequest build() { - return new TransformationRequest( - flattenForSlowMotion, outputHeight, audioMimeType, videoMimeType, hdrMode); + return new TransformationRequest(outputHeight, audioMimeType, videoMimeType, hdrMode); } } - /** - * Whether the input should be flattened for media containing slow motion markers. - * - * @see Builder#setFlattenForSlowMotion(boolean) - */ - public final boolean flattenForSlowMotion; /** * The requested height of the output video, or {@link C#LENGTH_UNSET} if inferred from the input. * @@ -329,13 +283,10 @@ public final class TransformationRequest { public final @HdrMode int hdrMode; private TransformationRequest( - boolean flattenForSlowMotion, int outputHeight, @Nullable String audioMimeType, @Nullable String videoMimeType, @HdrMode int hdrMode) { - - this.flattenForSlowMotion = flattenForSlowMotion; this.outputHeight = outputHeight; this.audioMimeType = audioMimeType; this.videoMimeType = videoMimeType; @@ -351,8 +302,7 @@ public final class TransformationRequest { return false; } TransformationRequest that = (TransformationRequest) o; - return flattenForSlowMotion == that.flattenForSlowMotion - && outputHeight == that.outputHeight + return outputHeight == that.outputHeight && Util.areEqual(audioMimeType, that.audioMimeType) && Util.areEqual(videoMimeType, that.videoMimeType) && hdrMode == that.hdrMode; @@ -360,8 +310,7 @@ public final class TransformationRequest { @Override public int hashCode() { - int result = (flattenForSlowMotion ? 1 : 0); - result = 31 * result + outputHeight; + int result = outputHeight; result = 31 * result + (audioMimeType != null ? audioMimeType.hashCode() : 0); result = 31 * result + (videoMimeType != null ? videoMimeType.hashCode() : 0); result = 31 * result + hdrMode; diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java index 2363920fe5..8abfbc8687 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java @@ -42,9 +42,6 @@ import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.effect.GlEffectsFrameProcessor; 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.lang.annotation.Documented; @@ -52,6 +49,7 @@ 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. @@ -86,9 +84,10 @@ public final class Transformer { private ImmutableList videoEffects; private boolean removeAudio; private boolean removeVideo; + private boolean flattenForSlowMotion; private boolean generateSilentAudio; private ListenerSet listeners; - @Nullable private AssetLoader.Factory assetLoaderFactory; + private AssetLoader.@MonotonicNonNull Factory assetLoaderFactory; private FrameProcessor.Factory frameProcessorFactory; private Codec.EncoderFactory encoderFactory; private Muxer.Factory muxerFactory; @@ -199,14 +198,14 @@ public final class Transformer { } /** - * @deprecated Use {@link TransformationRequest.Builder#setFlattenForSlowMotion(boolean)} - * instead. + * @deprecated Use {@link EditedMediaItem.Builder#setFlattenForSlowMotion(boolean)} to flatten + * the {@link EditedMediaItem} passed to {@link #startTransformation(EditedMediaItem, + * String)} or {@link #startTransformation(EditedMediaItem, ParcelFileDescriptor)} instead. */ @CanIgnoreReturnValue @Deprecated public Builder setFlattenForSlowMotion(boolean flattenForSlowMotion) { - transformationRequest = - transformationRequest.buildUpon().setFlattenForSlowMotion(flattenForSlowMotion).build(); + this.flattenForSlowMotion = flattenForSlowMotion; return this; } @@ -419,15 +418,8 @@ public final class Transformer { checkSampleMimeType(transformationRequest.videoMimeType); } if (assetLoaderFactory == null) { - DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); - if (transformationRequest.flattenForSlowMotion) { - defaultExtractorsFactory.setMp4ExtractorFlags(Mp4Extractor.FLAG_READ_SEF_DATA); - } - MediaSource.Factory mediaSourceFactory = - new DefaultMediaSourceFactory(context, defaultExtractorsFactory); - Codec.DecoderFactory decoderFactory = new DefaultDecoderFactory(context); assetLoaderFactory = - new DefaultAssetLoaderFactory(context, mediaSourceFactory, decoderFactory, clock); + new DefaultAssetLoaderFactory(context, new DefaultDecoderFactory(context), clock); } return new Transformer( context, @@ -436,6 +428,7 @@ public final class Transformer { videoEffects, removeAudio, removeVideo, + flattenForSlowMotion, generateSilentAudio, listeners, assetLoaderFactory, @@ -556,6 +549,7 @@ public final class Transformer { private final ImmutableList videoEffects; private final boolean removeAudio; private final boolean removeVideo; + private final boolean flattenForSlowMotion; private final boolean generateSilentAudio; private final ListenerSet listeners; private final AssetLoader.Factory assetLoaderFactory; @@ -575,6 +569,7 @@ public final class Transformer { ImmutableList videoEffects, boolean removeAudio, boolean removeVideo, + boolean flattenForSlowMotion, boolean generateSilentAudio, ListenerSet listeners, AssetLoader.Factory assetLoaderFactory, @@ -591,6 +586,7 @@ public final class Transformer { this.videoEffects = videoEffects; this.removeAudio = removeAudio; this.removeVideo = removeVideo; + this.flattenForSlowMotion = flattenForSlowMotion; this.generateSilentAudio = generateSilentAudio; this.listeners = listeners; this.assetLoaderFactory = assetLoaderFactory; @@ -712,10 +708,16 @@ public final class Transformer { */ @Deprecated public void startTransformation(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, frameProcessorFactory)) .build(); startTransformationInternal(editedMediaItem, path, /* parcelFileDescriptor= */ null); @@ -727,10 +729,16 @@ public final class Transformer { @Deprecated @RequiresApi(26) public void startTransformation(MediaItem mediaItem, ParcelFileDescriptor parcelFileDescriptor) { + 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, frameProcessorFactory)) .build(); startTransformationInternal(editedMediaItem, /* path= */ null, parcelFileDescriptor); @@ -740,22 +748,16 @@ public final class Transformer { EditedMediaItem editedMediaItem, @Nullable String path, @Nullable ParcelFileDescriptor parcelFileDescriptor) { - MediaItem mediaItem = editedMediaItem.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 (transformerInternal != null) { throw new IllegalStateException("There is already a transformation in progress."); } TransformerInternalListener transformerInternalListener = - new TransformerInternalListener(mediaItem); + new TransformerInternalListener(editedMediaItem.mediaItem); HandlerWrapper applicationHandler = clock.createHandler(looper, /* callback= */ null); FallbackListener fallbackListener = - new FallbackListener(mediaItem, listeners, applicationHandler, transformationRequest); + new FallbackListener( + editedMediaItem.mediaItem, listeners, applicationHandler, transformationRequest); transformerInternal = new TransformerInternal( context, 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 137e1901a5..d4b9bdf14f 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java @@ -35,7 +35,6 @@ 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.Metadata; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Clock; @@ -99,7 +98,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final HandlerThread internalHandlerThread; private final HandlerWrapper internalHandler; private final AssetLoader assetLoader; - private final Effects effects; private final List samplePipelines; private final MuxerWrapper muxerWrapper; private final ConditionVariable transformerConditionVariable; @@ -138,15 +136,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; internalHandlerThread = new HandlerThread("Transformer:Internal"); internalHandlerThread.start(); Looper internalLooper = internalHandlerThread.getLooper(); - MediaItem mediaItem = editedMediaItem.mediaItem; - ComponentListener componentListener = new ComponentListener(mediaItem, fallbackListener); + ComponentListener componentListener = new ComponentListener(editedMediaItem, fallbackListener); assetLoader = assetLoaderFactory .setRemoveAudio(editedMediaItem.removeAudio) .setRemoveVideo(editedMediaItem.removeVideo) - .setFlattenVideoForSlowMotion(transformationRequest.flattenForSlowMotion) - .createAssetLoader(mediaItem, internalLooper, componentListener); - effects = editedMediaItem.effects; + .setFlattenVideoForSlowMotion(editedMediaItem.flattenForSlowMotion) + .createAssetLoader(editedMediaItem.mediaItem, internalLooper, componentListener); samplePipelines = new ArrayList<>(); muxerWrapper = new MuxerWrapper(outputPath, outputParcelFileDescriptor, muxerFactory, componentListener); @@ -324,7 +320,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private class ComponentListener implements AssetLoader.Listener, MuxerWrapper.Listener { - private final MediaItem mediaItem; + private final EditedMediaItem editedMediaItem; private final FallbackListener fallbackListener; private final AtomicInteger trackCount; @@ -332,8 +328,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private volatile long durationUs; - public ComponentListener(MediaItem mediaItem, FallbackListener fallbackListener) { - this.mediaItem = mediaItem; + public ComponentListener(EditedMediaItem editedMediaItem, FallbackListener fallbackListener) { + this.editedMediaItem = editedMediaItem; this.fallbackListener = fallbackListener; trackCount = new AtomicInteger(); durationUs = C.TIME_UNSET; @@ -479,7 +475,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; streamStartPositionUs, streamOffsetUs, transformationRequest, - effects.audioProcessors, + editedMediaItem.flattenForSlowMotion, + editedMediaItem.effects.audioProcessors, generateSilentAudio ? durationUs : C.TIME_UNSET, encoderFactory, muxerWrapper, @@ -491,8 +488,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; streamStartPositionUs, streamOffsetUs, transformationRequest, - effects.videoEffects, - effects.frameProcessorFactory, + editedMediaItem.effects.videoEffects, + editedMediaItem.effects.frameProcessorFactory, encoderFactory, muxerWrapper, /* errorConsumer= */ this::onTransformationError, @@ -520,10 +517,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; && !muxerWrapper.supportsSampleMimeType(inputFormat.sampleMimeType)) { return true; } - if (transformationRequest.flattenForSlowMotion && isSlowMotion(inputFormat)) { + if (editedMediaItem.flattenForSlowMotion && isSlowMotion(inputFormat)) { return true; } - if (!effects.audioProcessors.isEmpty()) { + if (!editedMediaItem.effects.audioProcessors.isEmpty()) { return true; } if (generateSilentAudio) { @@ -549,7 +546,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private boolean shouldTranscodeVideo( Format inputFormat, long streamStartPositionUs, long streamOffsetUs) { if ((streamStartPositionUs - streamOffsetUs) != 0 - && !mediaItem.clippingConfiguration.startsAtKeyFrame) { + && !editedMediaItem.mediaItem.clippingConfiguration.startsAtKeyFrame) { return true; } if (encoderFactory.videoNeedsEncoding()) { @@ -571,8 +568,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } // TODO(b/265927935): consider generalizing this logic. - for (int i = 0; i < effects.videoEffects.size(); i++) { - Effect videoEffect = effects.videoEffects.get(i); + for (int i = 0; i < editedMediaItem.effects.videoEffects.size(); i++) { + Effect videoEffect = editedMediaItem.effects.videoEffects.get(i); if (videoEffect instanceof Presentation) { Presentation presentation = (Presentation) videoEffect; // The decoder rotates encoded frames for display by inputFormat.rotationDegrees. diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/EditedMediaItemBuilderTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/EditedMediaItemBuilderTest.java index 381d15415d..160db0222b 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/EditedMediaItemBuilderTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/EditedMediaItemBuilderTest.java @@ -38,4 +38,19 @@ public final class EditedMediaItemBuilderTest { .setRemoveVideo(true) .build()); } + + @Test + public void setFlattenForSlowMotion_forClippedMediaItem_throws() { + MediaItem.ClippingConfiguration clippingConfiguration = + new MediaItem.ClippingConfiguration.Builder().setStartPositionMs(1000).build(); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri("Uri") + .setClippingConfiguration(clippingConfiguration) + .build(); + + assertThrows( + IllegalArgumentException.class, + () -> new EditedMediaItem.Builder(mediaItem).setFlattenForSlowMotion(true).build()); + } } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/ExoPlayerAssetLoaderTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/ExoPlayerAssetLoaderTest.java index fd2077ba04..78ea52c03d 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/ExoPlayerAssetLoaderTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/ExoPlayerAssetLoaderTest.java @@ -27,8 +27,6 @@ import androidx.media3.common.Format; import androidx.media3.common.MediaItem; import androidx.media3.common.util.Clock; import androidx.media3.decoder.DecoderInputBuffer; -import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; -import androidx.media3.exoplayer.source.MediaSource; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.time.Duration; @@ -118,10 +116,9 @@ public class ExoPlayerAssetLoaderTest { private static AssetLoader getAssetLoader( Looper looper, AssetLoader.Listener listener, Clock clock) { Context context = ApplicationProvider.getApplicationContext(); - MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(context); Codec.DecoderFactory decoderFactory = new DefaultDecoderFactory(context); MediaItem mediaItem = MediaItem.fromUri("asset:///media/mp4/sample.mp4"); - return new ExoPlayerAssetLoader.Factory(context, mediaSourceFactory, decoderFactory, clock) + return new ExoPlayerAssetLoader.Factory(context, decoderFactory, clock) .setRemoveAudio(false) .setRemoveVideo(false) .setFlattenVideoForSlowMotion(false) diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformationRequestTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformationRequestTest.java index 36499606d8..0501e187a3 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformationRequestTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformationRequestTest.java @@ -16,6 +16,7 @@ package androidx.media3.transformer; +import static androidx.media3.transformer.TransformationRequest.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL; import static com.google.common.truth.Truth.assertThat; import androidx.media3.common.MimeTypes; @@ -35,9 +36,10 @@ public class TransformationRequestTest { private static TransformationRequest createTestTransformationRequest() { return new TransformationRequest.Builder() - .setFlattenForSlowMotion(true) + .setResolution(720) .setAudioMimeType(MimeTypes.AUDIO_AAC) .setVideoMimeType(MimeTypes.VIDEO_H264) + .setHdrMode(HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL) .build(); } } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java index 01dfbc408b..ecaf2718ce 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -502,13 +502,10 @@ public final class TransformerEndToEndTest { @Test public void startTransformation_flattenForSlowMotion_completesSuccessfully() throws Exception { - Transformer transformer = - createTransformerBuilder(/* enableFallback= */ false) - .setTransformationRequest( - new TransformationRequest.Builder().setFlattenForSlowMotion(true).build()) - .build(); + Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); EditedMediaItem editedMediaItem = new EditedMediaItem.Builder(MediaItem.fromUri(ASSET_URI_PREFIX + FILE_WITH_SEF_SLOW_MOTION)) + .setFlattenForSlowMotion(true) .build(); transformer.startTransformation(editedMediaItem, outputPath); @@ -645,7 +642,7 @@ public final class TransformerEndToEndTest { context, new SlowExtractorsFactory(/* delayBetweenReadsMs= */ 10)); Codec.DecoderFactory decoderFactory = new DefaultDecoderFactory(context); AssetLoader.Factory assetLoaderFactory = - new ExoPlayerAssetLoader.Factory(context, mediaSourceFactory, decoderFactory, clock); + new ExoPlayerAssetLoader.Factory(context, decoderFactory, clock, mediaSourceFactory); Muxer.Factory muxerFactory = new TestMuxerFactory(/* maxDelayBetweenSamplesMs= */ 1); Transformer transformer = createTransformerBuilder(/* enableFallback= */ false)