Allow video format change.

Uses the first mediaItem's format as the output format.

If there is `Presentation` supplied in the `Composition.effects`, add it as the
last effect of the first EditedMediaItem.

PiperOrigin-RevId: 512082659
This commit is contained in:
claincly 2023-02-24 16:56:42 +00:00 committed by tonihei
parent 0afe5923d8
commit c7b4ec4d65
4 changed files with 55 additions and 23 deletions

View File

@ -321,6 +321,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private synchronized boolean ensureConfigured(int inputWidth, int inputHeight) private synchronized boolean ensureConfigured(int inputWidth, int inputHeight)
throws VideoFrameProcessingException, GlUtil.GlException { throws VideoFrameProcessingException, GlUtil.GlException {
boolean inputSizeChanged = false;
if (this.inputWidth != inputWidth if (this.inputWidth != inputWidth
|| this.inputHeight != inputHeight || this.inputHeight != inputHeight
|| this.outputSizeBeforeSurfaceTransformation == null) { || this.outputSizeBeforeSurfaceTransformation == null) {
@ -337,6 +338,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
outputSizeBeforeSurfaceTransformation.getWidth(), outputSizeBeforeSurfaceTransformation.getWidth(),
outputSizeBeforeSurfaceTransformation.getHeight())); outputSizeBeforeSurfaceTransformation.getHeight()));
} }
inputSizeChanged = true;
} }
if (outputSurfaceInfo == null) { if (outputSurfaceInfo == null) {
@ -372,7 +374,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.debugSurfaceView = debugSurfaceView; this.debugSurfaceView = debugSurfaceView;
} }
if (defaultShaderProgram != null && outputSizeOrRotationChanged) { if (defaultShaderProgram != null && (outputSizeOrRotationChanged || inputSizeChanged)) {
defaultShaderProgram.release(); defaultShaderProgram.release();
defaultShaderProgram = null; defaultShaderProgram = null;
outputSizeOrRotationChanged = false; outputSizeOrRotationChanged = false;

View File

@ -39,6 +39,7 @@ import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.effect.DefaultVideoFrameProcessor; import androidx.media3.effect.DefaultVideoFrameProcessor;
import androidx.media3.effect.Presentation;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
@ -653,8 +654,12 @@ public final class Transformer {
* *
* <ul> * <ul>
* <li>The {@link Composition} must contain exactly one {@link Composition#sequences * <li>The {@link Composition} must contain exactly one {@link Composition#sequences
* EditedMediaItemSequence} and its {@link Composition#effects Effects} must be {@linkplain * EditedMediaItemSequence}. Its {@link Composition#effects Effects} must
* Effects#EMPTY empty}. * <ul>
* <li>Contain no {@linkplain Effects#audioProcessors audio effects}.
* <li>Either contain no {@linkplain Effects#videoEffects video effects}, or exactly one
* {@link Presentation}.
* </ul>
* <li>The {@link EditedMediaItem} instances in the {@link EditedMediaItemSequence} must: * <li>The {@link EditedMediaItem} instances in the {@link EditedMediaItemSequence} must:
* <ul> * <ul>
* <li>have identical tracks of the same format (after {@linkplain * <li>have identical tracks of the same format (after {@linkplain
@ -690,7 +695,12 @@ public final class Transformer {
*/ */
public void start(Composition composition, String path) { public void start(Composition composition, String path) {
checkArgument(composition.sequences.size() == 1); checkArgument(composition.sequences.size() == 1);
checkArgument(composition.effects == Effects.EMPTY); checkArgument(composition.effects.audioProcessors.isEmpty());
// Only supports Presentation in video effects.
ImmutableList<Effect> videoEffects = composition.effects.videoEffects;
checkArgument(
videoEffects.isEmpty()
|| (videoEffects.size() == 1 && videoEffects.get(0) instanceof Presentation));
verifyApplicationThread(); verifyApplicationThread();
checkState(transformerInternal == null, "There is already an export in progress."); checkState(transformerInternal == null, "There is already an export in progress.");

View File

@ -41,6 +41,7 @@ import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.effect.GlEffect; import androidx.media3.effect.GlEffect;
import androidx.media3.effect.Presentation;
import androidx.media3.extractor.metadata.mp4.SlowMotionData; import androidx.media3.extractor.metadata.mp4.SlowMotionData;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
@ -131,9 +132,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
internalHandlerThread.start(); internalHandlerThread.start();
Looper internalLooper = internalHandlerThread.getLooper(); Looper internalLooper = internalHandlerThread.getLooper();
EditedMediaItemSequence sequence = composition.sequences.get(0); EditedMediaItemSequence sequence = composition.sequences.get(0);
ImmutableList<Effect> compositionVideoEffects = composition.effects.videoEffects;
@Nullable
Presentation presentation =
compositionVideoEffects.isEmpty() ? null : (Presentation) compositionVideoEffects.get(0);
ComponentListener componentListener = ComponentListener componentListener =
new ComponentListener( new ComponentListener(
sequence, composition.transmuxAudio, composition.transmuxVideo, fallbackListener); sequence,
presentation,
composition.transmuxAudio,
composition.transmuxVideo,
fallbackListener);
compositeAssetLoader = compositeAssetLoader =
new CompositeAssetLoader( new CompositeAssetLoader(
sequence, sequence,
@ -313,6 +322,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// The first EditedMediaItem in the sequence determines which SamplePipeline to use. // The first EditedMediaItem in the sequence determines which SamplePipeline to use.
private final EditedMediaItem firstEditedMediaItem; private final EditedMediaItem firstEditedMediaItem;
@Nullable private final Presentation compositionPresentation;
private final int mediaItemCount; private final int mediaItemCount;
private final boolean transmuxAudio; private final boolean transmuxAudio;
private final boolean transmuxVideo; private final boolean transmuxVideo;
@ -323,10 +333,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public ComponentListener( public ComponentListener(
EditedMediaItemSequence sequence, EditedMediaItemSequence sequence,
@Nullable Presentation compositionPresentation,
boolean transmuxAudio, boolean transmuxAudio,
boolean transmuxVideo, boolean transmuxVideo,
FallbackListener fallbackListener) { FallbackListener fallbackListener) {
firstEditedMediaItem = sequence.editedMediaItems.get(0); firstEditedMediaItem = sequence.editedMediaItems.get(0);
this.compositionPresentation = compositionPresentation;
mediaItemCount = sequence.editedMediaItems.size(); mediaItemCount = sequence.editedMediaItems.size();
this.transmuxAudio = transmuxAudio; this.transmuxAudio = transmuxAudio;
this.transmuxVideo = transmuxVideo; this.transmuxVideo = transmuxVideo;
@ -458,6 +470,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
streamOffsetUs, streamOffsetUs,
transformationRequest, transformationRequest,
firstEditedMediaItem.effects.videoEffects, firstEditedMediaItem.effects.videoEffects,
compositionPresentation,
firstEditedMediaItem.effects.videoFrameProcessorFactory, firstEditedMediaItem.effects.videoFrameProcessorFactory,
encoderFactory, encoderFactory,
muxerWrapper, muxerWrapper,

View File

@ -41,11 +41,14 @@ import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.VideoFrameProcessor; import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.util.Consumer; import androidx.media3.common.util.Consumer;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.effect.Presentation;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -60,7 +63,6 @@ import org.checkerframework.dataflow.qual.Pure;
private final AtomicLong mediaItemOffsetUs; private final AtomicLong mediaItemOffsetUs;
private final VideoFrameProcessor videoFrameProcessor; private final VideoFrameProcessor videoFrameProcessor;
private final ColorInfo videoFrameProcessorInputColor; private final ColorInfo videoFrameProcessorInputColor;
private final FrameInfo firstFrameInfo;
private final EncoderWrapper encoderWrapper; private final EncoderWrapper encoderWrapper;
private final DecoderInputBuffer encoderOutputBuffer; private final DecoderInputBuffer encoderOutputBuffer;
@ -77,6 +79,7 @@ import org.checkerframework.dataflow.qual.Pure;
long streamOffsetUs, long streamOffsetUs,
TransformationRequest transformationRequest, TransformationRequest transformationRequest,
ImmutableList<Effect> effects, ImmutableList<Effect> effects,
@Nullable Presentation presentation,
VideoFrameProcessor.Factory videoFrameProcessorFactory, VideoFrameProcessor.Factory videoFrameProcessorFactory,
Codec.EncoderFactory encoderFactory, Codec.EncoderFactory encoderFactory,
MuxerWrapper muxerWrapper, MuxerWrapper muxerWrapper,
@ -84,6 +87,7 @@ import org.checkerframework.dataflow.qual.Pure;
FallbackListener fallbackListener, FallbackListener fallbackListener,
DebugViewProvider debugViewProvider) DebugViewProvider debugViewProvider)
throws ExportException { throws ExportException {
// TODO(b/262693177) Add tests for input format change.
super(firstInputFormat, streamStartPositionUs, muxerWrapper); super(firstInputFormat, streamStartPositionUs, muxerWrapper);
mediaItemOffsetUs = new AtomicLong(); mediaItemOffsetUs = new AtomicLong();
@ -119,11 +123,15 @@ import org.checkerframework.dataflow.qual.Pure;
.setColorTransfer(C.COLOR_TRANSFER_GAMMA_2_2) .setColorTransfer(C.COLOR_TRANSFER_GAMMA_2_2)
.build() .build()
: encoderInputColor; : encoderInputColor;
List<Effect> effectsWithPresentation = new ArrayList<>(effects);
if (presentation != null) {
effectsWithPresentation.add(presentation);
}
try { try {
videoFrameProcessor = videoFrameProcessor =
videoFrameProcessorFactory.create( videoFrameProcessorFactory.create(
context, context,
effects, effectsWithPresentation,
debugViewProvider, debugViewProvider,
videoFrameProcessorInputColor, videoFrameProcessorInputColor,
videoFrameProcessorOutputColor, videoFrameProcessorOutputColor,
@ -171,20 +179,6 @@ import org.checkerframework.dataflow.qual.Pure;
throw ExportException.createForVideoFrameProcessingException( throw ExportException.createForVideoFrameProcessingException(
e, ExportException.ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED); e, ExportException.ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED);
} }
// The decoder rotates encoded frames for display by firstInputFormat.rotationDegrees.
int decodedWidth =
(firstInputFormat.rotationDegrees % 180 == 0)
? firstInputFormat.width
: firstInputFormat.height;
int decodedHeight =
(firstInputFormat.rotationDegrees % 180 == 0)
? firstInputFormat.height
: firstInputFormat.width;
firstFrameInfo =
new FrameInfo.Builder(decodedWidth, decodedHeight)
.setPixelWidthHeightRatio(firstInputFormat.pixelWidthHeightRatio)
.setStreamOffsetUs(streamOffsetUs)
.build();
} }
@Override @Override
@ -193,8 +187,14 @@ import org.checkerframework.dataflow.qual.Pure;
long durationUs, long durationUs,
@Nullable Format trackFormat, @Nullable Format trackFormat,
boolean isLast) { boolean isLast) {
if (trackFormat != null) {
Size decodedSize = getDecodedSize(trackFormat);
videoFrameProcessor.setInputFrameInfo( videoFrameProcessor.setInputFrameInfo(
new FrameInfo.Builder(firstFrameInfo).setOffsetToAddUs(mediaItemOffsetUs.get()).build()); new FrameInfo.Builder(decodedSize.getWidth(), decodedSize.getHeight())
.setPixelWidthHeightRatio(trackFormat.pixelWidthHeightRatio)
.setOffsetToAddUs(mediaItemOffsetUs.get())
.build());
}
mediaItemOffsetUs.addAndGet(durationUs); mediaItemOffsetUs.addAndGet(durationUs);
} }
@ -314,6 +314,13 @@ import org.checkerframework.dataflow.qual.Pure;
return supportedRequestBuilder.build(); return supportedRequestBuilder.build();
} }
private static Size getDecodedSize(Format format) {
// The decoder rotates encoded frames for display by firstInputFormat.rotationDegrees.
int decodedWidth = (format.rotationDegrees % 180 == 0) ? format.width : format.height;
int decodedHeight = (format.rotationDegrees % 180 == 0) ? format.height : format.width;
return new Size(decodedWidth, decodedHeight);
}
/** /**
* Wraps an {@linkplain Codec encoder} and provides its input {@link Surface}. * Wraps an {@linkplain Codec encoder} and provides its input {@link Surface}.
* *