Support transmuxing when no op effects and regular rotations are set
PiperOrigin-RevId: 601419245
This commit is contained in:
parent
79b0b8090c
commit
f9eb8626eb
@ -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<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);
|
||||
EditedMediaItem editedMediaItem =
|
||||
new EditedMediaItem.Builder(mediaItem).setEffects(effects).build();
|
||||
|
@ -144,20 +144,24 @@ import com.google.common.collect.ImmutableList;
|
||||
}
|
||||
ImmutableList<Effect> 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.
|
||||
*
|
||||
* <p>If {@code true}, sets the regular rotation on the {@linkplain
|
||||
* MuxerWrapper#setAdditionalRotationDegrees}.
|
||||
*/
|
||||
public static boolean areVideoEffectsAllNoOp(
|
||||
ImmutableList<Effect> videoEffects, Format inputFormat) {
|
||||
private static boolean areVideoEffectsAllRegularRotationsOrNoOp(
|
||||
ImmutableList<Effect> 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<Effect> 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;
|
||||
|
@ -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<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
|
||||
public void getProgress_unknownDuration_returnsConsistentStates() throws Exception {
|
||||
Transformer transformer =
|
||||
|
@ -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) {}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user