diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java index 971efef128..a257d2ddc3 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -327,7 +327,10 @@ public class TransformerEndToEndTest { new DefaultEncoderFactory.Builder(context).setEnableFallback(false).build()) .build(); MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_ASSET_URI_STRING)); - ImmutableList videoEffects = ImmutableList.of(Presentation.createForHeight(480)); + ImmutableList videoEffects = + ImmutableList.of( + new ScaleAndRotateTransformation.Builder().setRotationDegrees(90).build(), + Presentation.createForHeight(MP4_ASSET_FORMAT.height)); Effects effects = new Effects(/* audioProcessors= */ ImmutableList.of(), videoEffects); EditedMediaItem editedMediaItem = new EditedMediaItem.Builder(mediaItem).setEffects(effects).build(); 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 1c7fb37d3e..73a7c06cbb 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerUtil.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerUtil.java @@ -144,20 +144,24 @@ import com.google.common.collect.ImmutableList; } ImmutableList videoEffects = firstEditedMediaItem.effects.videoEffects; return !videoEffects.isEmpty() - && !areVideoEffectsAllNoOp(videoEffects, inputFormat) - && !hasOnlyRegularRotationEffect(videoEffects, muxerWrapper); + && !areVideoEffectsAllRegularRotationsOrNoOp(videoEffects, inputFormat, 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}. + * Returns whether the effects, applied in the list ordering, would result in a noOp or regular + * rotation. + * + *

If {@code true}, sets the regular rotation on the {@linkplain + * MuxerWrapper#setAdditionalRotationDegrees}. */ - public static boolean areVideoEffectsAllNoOp( - ImmutableList videoEffects, Format inputFormat) { + private static boolean areVideoEffectsAllRegularRotationsOrNoOp( + ImmutableList videoEffects, Format inputFormat, MuxerWrapper muxerWrapper) { int decodedWidth = (inputFormat.rotationDegrees % 180 == 0) ? inputFormat.width : inputFormat.height; int decodedHeight = (inputFormat.rotationDegrees % 180 == 0) ? inputFormat.height : inputFormat.width; + boolean widthHeightFlipped = false; + float totalRotationDegrees = 0; for (int i = 0; i < videoEffects.size(); i++) { Effect videoEffect = videoEffects.get(i); if (!(videoEffect instanceof GlEffect)) { @@ -166,32 +170,42 @@ import com.google.common.collect.ImmutableList; return false; } GlEffect glEffect = (GlEffect) videoEffect; + if (videoEffect instanceof ScaleAndRotateTransformation) { + ScaleAndRotateTransformation scaleAndRotateTransformation = + (ScaleAndRotateTransformation) videoEffect; + if (scaleAndRotateTransformation.scaleX != 1f + || scaleAndRotateTransformation.scaleY != 1f) { + return false; + } + float rotationDegrees = scaleAndRotateTransformation.rotationDegrees; + totalRotationDegrees += rotationDegrees; + if (totalRotationDegrees % 90 == 0 && !widthHeightFlipped) { + int temp = decodedWidth; + decodedWidth = decodedHeight; + decodedHeight = temp; + widthHeightFlipped = true; + } else if (totalRotationDegrees % 180 == 0 && widthHeightFlipped) { + int temp = decodedWidth; + decodedWidth = decodedHeight; + decodedHeight = temp; + widthHeightFlipped = false; + } + continue; + } if (!glEffect.isNoOp(decodedWidth, decodedHeight)) { return false; } } - return true; - } - - private static boolean hasOnlyRegularRotationEffect( - ImmutableList videoEffects, MuxerWrapper muxerWrapper) { - if (videoEffects.size() != 1) { - return false; + totalRotationDegrees %= 360; + if (totalRotationDegrees == 0) { + 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) { + if (totalRotationDegrees == 90f + || totalRotationDegrees == 180f + || totalRotationDegrees == 270f) { // The MuxerWrapper rotation is clockwise while the ScaleAndRotateTransformation rotation // is counterclockwise. - muxerWrapper.setAdditionalRotationDegrees(360 - Math.round(rotationDegrees)); + muxerWrapper.setAdditionalRotationDegrees(360 - Math.round(totalRotationDegrees)); return true; } return false; diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java index 169e12637d..30a089e95e 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java @@ -66,6 +66,7 @@ import androidx.media3.common.Format; import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; import androidx.media3.common.audio.SonicAudioProcessor; +import androidx.media3.effect.Contrast; import androidx.media3.effect.Presentation; import androidx.media3.effect.ScaleAndRotateTransformation; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; @@ -1065,6 +1066,34 @@ public final class MediaItemExportTest { /* originalFileName= */ FILE_AUDIO_VIDEO, /* modifications...= */ "rotated")); } + @Test + public void start_totalRotationRegularAndNoOps_transmuxes() throws Exception { + Transformer transformer = + createTransformerBuilder(muxerFactory, /* enableFallback= */ false).build(); + // Total rotation is 270. + ImmutableList videoEffects = + ImmutableList.of( + new ScaleAndRotateTransformation.Builder().setRotationDegrees(315).build(), + new Contrast(0f), + new ScaleAndRotateTransformation.Builder().setRotationDegrees(180).build(), + new ScaleAndRotateTransformation.Builder().setRotationDegrees(135).build(), + Presentation.createForHeight(1080)); + EditedMediaItem editedMediaItem = + new EditedMediaItem.Builder(MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO)) + .setEffects(new Effects(ImmutableList.of(), videoEffects)) + .build(); + + transformer.start(editedMediaItem, outputDir.newFile().getPath()); + TransformerTestRunner.runLooper(transformer); + + // Video transcoding in unit tests is not supported. + DumpFileAsserts.assertOutput( + context, + muxerFactory.getCreatedMuxer(), + getDumpFileName( + /* originalFileName= */ FILE_AUDIO_VIDEO, /* modifications...= */ "rotated")); + } + @Test public void getProgress_unknownDuration_returnsConsistentStates() throws Exception { Transformer transformer = diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerUtilTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerUtilTest.java new file mode 100644 index 0000000000..433406b80d --- /dev/null +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerUtilTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2023 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.MimeTypes.VIDEO_H264; +import static androidx.media3.transformer.MuxerWrapper.MUXER_MODE_DEFAULT; +import static androidx.media3.transformer.TestUtil.ASSET_URI_PREFIX; +import static androidx.media3.transformer.TestUtil.FILE_AUDIO_VIDEO; +import static androidx.media3.transformer.TransformerUtil.shouldTranscodeVideo; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static com.google.common.truth.Truth.assertThat; + +import androidx.media3.common.C; +import androidx.media3.common.Effect; +import androidx.media3.common.Format; +import androidx.media3.common.MediaItem; +import androidx.media3.effect.Presentation; +import androidx.media3.effect.ScaleAndRotateTransformation; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +/** Unit tests for {@link TransformerUtil}. */ +@RunWith(AndroidJUnit4.class) +public class TransformerUtilTest { + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void shouldTranscodeVideo_regularRotationAndTranscodingPresentation_returnsTrue() + throws Exception { + MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO); + Format format = + new Format.Builder() + .setSampleMimeType(VIDEO_H264) + .setWidth(1080) + .setHeight(720) + .setFrameRate(29.97f) + .setCodecs("avc1.64001F") + .build(); + ImmutableList videoEffects = + ImmutableList.of( + new ScaleAndRotateTransformation.Builder().setRotationDegrees(90).build(), + Presentation.createForHeight(format.height)); + Effects effects = new Effects(/* audioProcessors= */ ImmutableList.of(), videoEffects); + EditedMediaItem editedMediaItem = + new EditedMediaItem.Builder(mediaItem).setEffects(effects).build(); + Composition composition = + new Composition.Builder(new EditedMediaItemSequence(editedMediaItem)).build(); + MuxerWrapper muxerWrapper = + new MuxerWrapper( + temporaryFolder.newFile().getPath(), + new DefaultMuxer.Factory(), + new NoOpMuxerListenerImpl(), + MUXER_MODE_DEFAULT, + /* dropSamplesBeforeFirstVideoSample= */ false); + + assertThat( + shouldTranscodeVideo( + format, + composition, + /* sequenceIndex= */ 0, + new TransformationRequest.Builder().build(), + new DefaultEncoderFactory.Builder(getApplicationContext()).build(), + muxerWrapper)) + .isTrue(); + } + + private static final class NoOpMuxerListenerImpl implements MuxerWrapper.Listener { + + @Override + public void onTrackEnded( + @C.TrackType int trackType, Format format, int averageBitrate, int sampleCount) {} + + @Override + public void onEnded(long durationMs, long fileSizeBytes) {} + + @Override + public void onError(ExportException exportException) {} + } +}