Allow FrameProcessor input surface size changes.

This will be useful for downgrading to a lower resolution during
a slow preview and for processing slide-shows once sequential
multi-asset editing is supported.

PiperOrigin-RevId: 457017255
This commit is contained in:
hschlueter 2022-06-24 16:31:31 +01:00 committed by Ian Baker
parent 638c758721
commit 30c52c58f0
5 changed files with 78 additions and 34 deletions

View File

@ -367,8 +367,6 @@ public final class GlEffectsFrameProcessorPixelTest {
} }
}, },
pixelWidthHeightRatio, pixelWidthHeightRatio,
inputWidth,
inputHeight,
/* streamOffsetUs= */ 0L, /* streamOffsetUs= */ 0L,
effects, effects,
/* outputSurfaceProvider= */ (requestedWidth, requestedHeight) -> { /* outputSurfaceProvider= */ (requestedWidth, requestedHeight) -> {
@ -383,6 +381,7 @@ public final class GlEffectsFrameProcessorPixelTest {
}, },
Transformer.DebugViewProvider.NONE, Transformer.DebugViewProvider.NONE,
/* enableExperimentalHdrEditing= */ false)); /* enableExperimentalHdrEditing= */ false));
glEffectsFrameProcessor.setInputFrameInfo(new FrameInfo(inputWidth, inputHeight));
glEffectsFrameProcessor.registerInputFrame(); glEffectsFrameProcessor.registerInputFrame();
// Queue the first video frame from the extractor. // Queue the first video frame from the extractor.

View File

@ -0,0 +1,37 @@
/*
* Copyright 2022 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.util.Assertions.checkArgument;
/** Value class specifying information about a decoded video frame. */
/* package */ class FrameInfo {
/** The width of the frame, in pixels. */
public final int width;
/** The height of the frame, in pixels. */
public final int height;
// TODO(b/227625423): Add pixelWidthHeightRatio.
// TODO(b/227624622): Add color space information for HDR.
public FrameInfo(int width, int height) {
checkArgument(width > 0, "width must be positive, but is: " + width);
checkArgument(height > 0, "height must be positive, but is: " + height);
this.width = width;
this.height = height;
}
}

View File

@ -41,12 +41,21 @@ import android.view.Surface;
/** Returns the input {@link Surface}. */ /** Returns the input {@link Surface}. */
Surface getInputSurface(); Surface getInputSurface();
/**
* Sets information about the input frames.
*
* <p>The new input information is applied from the next frame {@linkplain #registerInputFrame()
* registered} onwards.
*/
void setInputFrameInfo(FrameInfo inputFrameInfo);
/** /**
* Informs the {@code FrameProcessor} that a frame will be queued to its input surface. * Informs the {@code FrameProcessor} that a frame will be queued to its input surface.
* *
* <p>Must be called before rendering a frame to the frame processor's input surface. * <p>Must be called before rendering a frame to the frame processor's input surface.
* *
* @throws IllegalStateException If called after {@link #signalEndOfInputStream()}. * @throws IllegalStateException If called after {@link #signalEndOfInputStream()} or before
* {@link #setInputFrameInfo(FrameInfo)}.
*/ */
void registerInputFrame(); void registerInputFrame();

View File

@ -15,8 +15,8 @@
*/ */
package androidx.media3.transformer; package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context; import android.content.Context;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
@ -31,10 +31,11 @@ import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* A {@link FrameProcessor} implementation that applies {@link GlEffect} instances using OpenGL on a * A {@link FrameProcessor} implementation that applies {@link GlEffect} instances using OpenGL on a
@ -51,8 +52,6 @@ import java.util.concurrent.atomic.AtomicInteger;
* @param listener A {@link Listener}. * @param listener A {@link Listener}.
* @param pixelWidthHeightRatio The ratio of width over height for each pixel. Pixels are expanded * @param pixelWidthHeightRatio The ratio of width over height for each pixel. Pixels are expanded
* by this ratio so that the output frame's pixels have a ratio of 1. * by this ratio so that the output frame's pixels have a ratio of 1.
* @param inputWidth The input frame width, in pixels.
* @param inputHeight The input frame height, in pixels.
* @param effects The {@link GlEffect GlEffects} to apply to each frame. * @param effects The {@link GlEffect GlEffects} to apply to each frame.
* @param outputSurfaceProvider A {@link SurfaceInfo.Provider} managing the output {@link * @param outputSurfaceProvider A {@link SurfaceInfo.Provider} managing the output {@link
* Surface}. * Surface}.
@ -66,16 +65,12 @@ import java.util.concurrent.atomic.AtomicInteger;
Context context, Context context,
FrameProcessor.Listener listener, FrameProcessor.Listener listener,
float pixelWidthHeightRatio, float pixelWidthHeightRatio,
int inputWidth,
int inputHeight,
long streamOffsetUs, long streamOffsetUs,
List<GlEffect> effects, List<GlEffect> effects,
SurfaceInfo.Provider outputSurfaceProvider, SurfaceInfo.Provider outputSurfaceProvider,
Transformer.DebugViewProvider debugViewProvider, Transformer.DebugViewProvider debugViewProvider,
boolean enableExperimentalHdrEditing) boolean enableExperimentalHdrEditing)
throws FrameProcessingException { throws FrameProcessingException {
checkArgument(inputWidth > 0, "inputWidth must be positive");
checkArgument(inputHeight > 0, "inputHeight must be positive");
ExecutorService singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME); ExecutorService singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME);
@ -86,8 +81,6 @@ import java.util.concurrent.atomic.AtomicInteger;
context, context,
listener, listener,
pixelWidthHeightRatio, pixelWidthHeightRatio,
inputWidth,
inputHeight,
streamOffsetUs, streamOffsetUs,
effects, effects,
outputSurfaceProvider, outputSurfaceProvider,
@ -118,8 +111,6 @@ import java.util.concurrent.atomic.AtomicInteger;
Context context, Context context,
FrameProcessor.Listener listener, FrameProcessor.Listener listener,
float pixelWidthHeightRatio, float pixelWidthHeightRatio,
int inputWidth,
int inputHeight,
long streamOffsetUs, long streamOffsetUs,
List<GlEffect> effects, List<GlEffect> effects,
SurfaceInfo.Provider outputSurfaceProvider, SurfaceInfo.Provider outputSurfaceProvider,
@ -177,8 +168,7 @@ import java.util.concurrent.atomic.AtomicInteger;
eglContext, eglContext,
frameProcessingTaskExecutor, frameProcessingTaskExecutor,
streamOffsetUs, streamOffsetUs,
/* inputExternalTexture= */ new TextureInfo( /* inputExternalTextureId= */ GlUtil.createExternalTexture(),
GlUtil.createExternalTexture(), /* fboId= */ C.INDEX_UNSET, inputWidth, inputHeight),
externalTextureProcessor, externalTextureProcessor,
textureProcessors); textureProcessors);
} }
@ -300,21 +290,19 @@ import java.util.concurrent.atomic.AtomicInteger;
* timestamps, in microseconds. * timestamps, in microseconds.
*/ */
private final long streamOffsetUs; private final long streamOffsetUs;
/**
* Number of frames {@linkplain #registerInputFrame() registered} but not processed off the {@link
* #inputSurfaceTexture} yet.
*/
private final AtomicInteger pendingInputFrameCount;
/** Associated with an OpenGL external texture. */ /** Associated with an OpenGL external texture. */
private final SurfaceTexture inputSurfaceTexture; private final SurfaceTexture inputSurfaceTexture;
/** Wraps the {@link #inputSurfaceTexture}. */ /** Wraps the {@link #inputSurfaceTexture}. */
private final Surface inputSurface; private final Surface inputSurface;
private final float[] inputSurfaceTextureTransformMatrix; private final float[] inputSurfaceTextureTransformMatrix;
private final TextureInfo inputExternalTexture; private final int inputExternalTextureId;
private final ExternalTextureProcessor inputExternalTextureProcessor; private final ExternalTextureProcessor inputExternalTextureProcessor;
private final ImmutableList<GlTextureProcessor> textureProcessors; private final ImmutableList<GlTextureProcessor> textureProcessors;
private final ConcurrentLinkedQueue<FrameInfo> pendingInputFrames;
private @MonotonicNonNull FrameInfo nextInputFrameInfo;
private boolean inputStreamEnded; private boolean inputStreamEnded;
private GlEffectsFrameProcessor( private GlEffectsFrameProcessor(
@ -322,7 +310,7 @@ import java.util.concurrent.atomic.AtomicInteger;
EGLContext eglContext, EGLContext eglContext,
FrameProcessingTaskExecutor frameProcessingTaskExecutor, FrameProcessingTaskExecutor frameProcessingTaskExecutor,
long streamOffsetUs, long streamOffsetUs,
TextureInfo inputExternalTexture, int inputExternalTextureId,
ExternalTextureProcessor inputExternalTextureProcessor, ExternalTextureProcessor inputExternalTextureProcessor,
ImmutableList<GlTextureProcessor> textureProcessors) { ImmutableList<GlTextureProcessor> textureProcessors) {
checkState(!textureProcessors.isEmpty()); checkState(!textureProcessors.isEmpty());
@ -331,33 +319,40 @@ import java.util.concurrent.atomic.AtomicInteger;
this.eglContext = eglContext; this.eglContext = eglContext;
this.frameProcessingTaskExecutor = frameProcessingTaskExecutor; this.frameProcessingTaskExecutor = frameProcessingTaskExecutor;
this.streamOffsetUs = streamOffsetUs; this.streamOffsetUs = streamOffsetUs;
this.inputExternalTexture = inputExternalTexture; this.inputExternalTextureId = inputExternalTextureId;
this.inputExternalTextureProcessor = inputExternalTextureProcessor; this.inputExternalTextureProcessor = inputExternalTextureProcessor;
this.textureProcessors = textureProcessors; this.textureProcessors = textureProcessors;
pendingInputFrameCount = new AtomicInteger(); inputSurfaceTexture = new SurfaceTexture(inputExternalTextureId);
inputSurfaceTexture = new SurfaceTexture(inputExternalTexture.texId);
inputSurface = new Surface(inputSurfaceTexture); inputSurface = new Surface(inputSurfaceTexture);
inputSurfaceTextureTransformMatrix = new float[16]; inputSurfaceTextureTransformMatrix = new float[16];
pendingInputFrames = new ConcurrentLinkedQueue<>();
} }
@Override @Override
public Surface getInputSurface() { public Surface getInputSurface() {
// TODO(b/227625423): Allow input surface to be recreated for input size change.
inputSurfaceTexture.setOnFrameAvailableListener( inputSurfaceTexture.setOnFrameAvailableListener(
surfaceTexture -> frameProcessingTaskExecutor.submit(this::processInputFrame)); surfaceTexture -> frameProcessingTaskExecutor.submit(this::processInputFrame));
return inputSurface; return inputSurface;
} }
@Override
public void setInputFrameInfo(FrameInfo inputFrameInfo) {
nextInputFrameInfo = inputFrameInfo;
}
@Override @Override
public void registerInputFrame() { public void registerInputFrame() {
checkState(!inputStreamEnded); checkState(!inputStreamEnded);
pendingInputFrameCount.incrementAndGet(); checkStateNotNull(
nextInputFrameInfo, "setInputFrameInfo must be called before registering input frames");
pendingInputFrames.add(nextInputFrameInfo);
} }
@Override @Override
public int getPendingInputFrameCount() { public int getPendingInputFrameCount() {
return pendingInputFrameCount.get(); return pendingInputFrames.size();
} }
@Override @Override
@ -401,10 +396,15 @@ import java.util.concurrent.atomic.AtomicInteger;
long presentationTimeUs = inputFrameTimeNs / 1000 - streamOffsetUs; long presentationTimeUs = inputFrameTimeNs / 1000 - streamOffsetUs;
inputSurfaceTexture.getTransformMatrix(inputSurfaceTextureTransformMatrix); inputSurfaceTexture.getTransformMatrix(inputSurfaceTextureTransformMatrix);
inputExternalTextureProcessor.setTextureTransformMatrix(inputSurfaceTextureTransformMatrix); inputExternalTextureProcessor.setTextureTransformMatrix(inputSurfaceTextureTransformMatrix);
FrameInfo inputFrameInfo = pendingInputFrames.remove();
checkState( checkState(
inputExternalTextureProcessor.maybeQueueInputFrame( inputExternalTextureProcessor.maybeQueueInputFrame(
inputExternalTexture, presentationTimeUs)); new TextureInfo(
checkState(pendingInputFrameCount.getAndDecrement() > 0); inputExternalTextureId,
/* fboId= */ C.INDEX_UNSET,
inputFrameInfo.width,
inputFrameInfo.height),
presentationTimeUs));
// After the inputExternalTextureProcessor has produced an output frame, it is processed // After the inputExternalTextureProcessor has produced an output frame, it is processed
// asynchronously by the texture processors chained after it. // asynchronously by the texture processors chained after it.
} }

View File

@ -120,8 +120,6 @@ import org.checkerframework.dataflow.qual.Pure;
} }
}, },
inputFormat.pixelWidthHeightRatio, inputFormat.pixelWidthHeightRatio,
/* inputWidth= */ decodedWidth,
/* inputHeight= */ decodedHeight,
streamOffsetUs, streamOffsetUs,
effectsListBuilder.build(), effectsListBuilder.build(),
/* outputSurfaceProvider= */ encoderWrapper, /* outputSurfaceProvider= */ encoderWrapper,
@ -131,6 +129,7 @@ import org.checkerframework.dataflow.qual.Pure;
throw TransformationException.createForFrameProcessingException( throw TransformationException.createForFrameProcessingException(
e, TransformationException.ERROR_CODE_GL_INIT_FAILED); e, TransformationException.ERROR_CODE_GL_INIT_FAILED);
} }
frameProcessor.setInputFrameInfo(new FrameInfo(decodedWidth, decodedHeight));
decoder = decoder =
decoderFactory.createForVideoDecoding( decoderFactory.createForVideoDecoding(