Support transmuxing when no op effects and regular rotations are set

PiperOrigin-RevId: 601419245
This commit is contained in:
tofunmi 2024-01-25 05:31:36 -08:00 committed by Copybara-Service
parent 79b0b8090c
commit f9eb8626eb
4 changed files with 168 additions and 26 deletions

View File

@ -327,7 +327,10 @@ public class TransformerEndToEndTest {
new DefaultEncoderFactory.Builder(context).setEnableFallback(false).build()) new DefaultEncoderFactory.Builder(context).setEnableFallback(false).build())
.build(); .build();
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_ASSET_URI_STRING)); MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_ASSET_URI_STRING));
ImmutableList<Effect> videoEffects = ImmutableList.of(Presentation.createForHeight(480)); ImmutableList<Effect> videoEffects =
ImmutableList.of(
new ScaleAndRotateTransformation.Builder().setRotationDegrees(90).build(),
Presentation.createForHeight(MP4_ASSET_FORMAT.height));
Effects effects = new Effects(/* audioProcessors= */ ImmutableList.of(), videoEffects); Effects effects = new Effects(/* audioProcessors= */ ImmutableList.of(), videoEffects);
EditedMediaItem editedMediaItem = EditedMediaItem editedMediaItem =
new EditedMediaItem.Builder(mediaItem).setEffects(effects).build(); new EditedMediaItem.Builder(mediaItem).setEffects(effects).build();

View File

@ -144,20 +144,24 @@ import com.google.common.collect.ImmutableList;
} }
ImmutableList<Effect> videoEffects = firstEditedMediaItem.effects.videoEffects; ImmutableList<Effect> videoEffects = firstEditedMediaItem.effects.videoEffects;
return !videoEffects.isEmpty() return !videoEffects.isEmpty()
&& !areVideoEffectsAllNoOp(videoEffects, inputFormat) && !areVideoEffectsAllRegularRotationsOrNoOp(videoEffects, inputFormat, muxerWrapper);
&& !hasOnlyRegularRotationEffect(videoEffects, muxerWrapper);
} }
/** /**
* Returns whether the collection of {@code videoEffects} would be a {@linkplain * Returns whether the effects, applied in the list ordering, would result in a noOp or regular
* GlEffect#isNoOp(int, int) no-op}, if queued samples of this {@link Format}. * rotation.
*
* <p>If {@code true}, sets the regular rotation on the {@linkplain
* MuxerWrapper#setAdditionalRotationDegrees}.
*/ */
public static boolean areVideoEffectsAllNoOp( private static boolean areVideoEffectsAllRegularRotationsOrNoOp(
ImmutableList<Effect> videoEffects, Format inputFormat) { ImmutableList<Effect> videoEffects, Format inputFormat, MuxerWrapper muxerWrapper) {
int decodedWidth = int decodedWidth =
(inputFormat.rotationDegrees % 180 == 0) ? inputFormat.width : inputFormat.height; (inputFormat.rotationDegrees % 180 == 0) ? inputFormat.width : inputFormat.height;
int decodedHeight = int decodedHeight =
(inputFormat.rotationDegrees % 180 == 0) ? inputFormat.height : inputFormat.width; (inputFormat.rotationDegrees % 180 == 0) ? inputFormat.height : inputFormat.width;
boolean widthHeightFlipped = false;
float totalRotationDegrees = 0;
for (int i = 0; i < videoEffects.size(); i++) { for (int i = 0; i < videoEffects.size(); i++) {
Effect videoEffect = videoEffects.get(i); Effect videoEffect = videoEffects.get(i);
if (!(videoEffect instanceof GlEffect)) { if (!(videoEffect instanceof GlEffect)) {
@ -166,32 +170,42 @@ import com.google.common.collect.ImmutableList;
return false; return false;
} }
GlEffect glEffect = (GlEffect) videoEffect; 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)) { if (!glEffect.isNoOp(decodedWidth, decodedHeight)) {
return false; return false;
} }
} }
return true; totalRotationDegrees %= 360;
} if (totalRotationDegrees == 0) {
return true;
private static boolean hasOnlyRegularRotationEffect(
ImmutableList<Effect> videoEffects, MuxerWrapper muxerWrapper) {
if (videoEffects.size() != 1) {
return false;
} }
Effect videoEffect = videoEffects.get(0); if (totalRotationDegrees == 90f
if (!(videoEffect instanceof ScaleAndRotateTransformation)) { || totalRotationDegrees == 180f
return false; || totalRotationDegrees == 270f) {
}
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 // The MuxerWrapper rotation is clockwise while the ScaleAndRotateTransformation rotation
// is counterclockwise. // is counterclockwise.
muxerWrapper.setAdditionalRotationDegrees(360 - Math.round(rotationDegrees)); muxerWrapper.setAdditionalRotationDegrees(360 - Math.round(totalRotationDegrees));
return true; return true;
} }
return false; return false;

View File

@ -66,6 +66,7 @@ import androidx.media3.common.Format;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.audio.SonicAudioProcessor; import androidx.media3.common.audio.SonicAudioProcessor;
import androidx.media3.effect.Contrast;
import androidx.media3.effect.Presentation; import androidx.media3.effect.Presentation;
import androidx.media3.effect.ScaleAndRotateTransformation; import androidx.media3.effect.ScaleAndRotateTransformation;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
@ -1065,6 +1066,34 @@ public final class MediaItemExportTest {
/* originalFileName= */ FILE_AUDIO_VIDEO, /* modifications...= */ "rotated")); /* 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<Effect> 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 @Test
public void getProgress_unknownDuration_returnsConsistentStates() throws Exception { public void getProgress_unknownDuration_returnsConsistentStates() throws Exception {
Transformer transformer = Transformer transformer =

View File

@ -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<Effect> 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) {}
}
}