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:
parent
0afe5923d8
commit
c7b4ec4d65
@ -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;
|
||||||
|
@ -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.");
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
videoFrameProcessor.setInputFrameInfo(
|
if (trackFormat != null) {
|
||||||
new FrameInfo.Builder(firstFrameInfo).setOffsetToAddUs(mediaItemOffsetUs.get()).build());
|
Size decodedSize = getDecodedSize(trackFormat);
|
||||||
|
videoFrameProcessor.setInputFrameInfo(
|
||||||
|
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}.
|
||||||
*
|
*
|
||||||
|
Loading…
x
Reference in New Issue
Block a user