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())
|
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();
|
||||||
|
@ -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;
|
||||||
|
@ -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 =
|
||||||
|
@ -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