Effect: Move inputColorInfo usage to registerInputStream.

To prepare to move `inputColorInfo` from `VFP.Factory.create` to
`VFP.registerInputStream`, move all usage of `inputColorInfo` to be *after*
`registerInputStream`.

To do this, defer creation of `externalShaderProgram` instances, which require
`inputColorInfo`. However, we must still initialize `InputSwitcher` and OpenGL
ES 3.0 contexts in the VFP create, to create and request the input surface from
ExternalTextureManager.

PiperOrigin-RevId: 590937251
This commit is contained in:
huangdarwin 2023-12-14 07:45:58 -08:00 committed by Copybara-Service
parent 01578780a6
commit 97e9ed3f7b
6 changed files with 176 additions and 136 deletions

View File

@ -45,7 +45,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+ " RGB channel."; + " RGB channel.";
private final GlObjectsProvider glObjectsProvider; private final GlObjectsProvider glObjectsProvider;
private final GlShaderProgram shaderProgram; private @MonotonicNonNull GlShaderProgram shaderProgram;
// The queue holds all bitmaps with one or more frames pending to be sent downstream. // The queue holds all bitmaps with one or more frames pending to be sent downstream.
private final Queue<BitmapFrameSequenceInfo> pendingBitmaps; private final Queue<BitmapFrameSequenceInfo> pendingBitmaps;
@ -63,21 +63,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* Creates a new instance. * Creates a new instance.
* *
* @param glObjectsProvider The {@link GlObjectsProvider} for using EGL and GLES. * @param glObjectsProvider The {@link GlObjectsProvider} for using EGL and GLES.
* @param shaderProgram The {@link GlShaderProgram} for which this {@code BitmapTextureManager}
* will be set as the {@link GlShaderProgram.InputListener}.
* @param videoFrameProcessingTaskExecutor The {@link VideoFrameProcessingTaskExecutor} that the * @param videoFrameProcessingTaskExecutor The {@link VideoFrameProcessingTaskExecutor} that the
* methods of this class run on. * methods of this class run on.
*/ */
public BitmapTextureManager( public BitmapTextureManager(
GlObjectsProvider glObjectsProvider, GlObjectsProvider glObjectsProvider,
GlShaderProgram shaderProgram,
VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor) { VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor) {
super(videoFrameProcessingTaskExecutor); super(videoFrameProcessingTaskExecutor);
this.glObjectsProvider = glObjectsProvider; this.glObjectsProvider = glObjectsProvider;
this.shaderProgram = shaderProgram;
pendingBitmaps = new LinkedBlockingQueue<>(); pendingBitmaps = new LinkedBlockingQueue<>();
} }
@Override
public void setSamplingGlShaderProgram(GlShaderProgram samplingGlShaderProgram) {
this.shaderProgram = samplingGlShaderProgram;
}
@Override @Override
public void onReadyToAcceptInputFrame() { public void onReadyToAcceptInputFrame() {
videoFrameProcessingTaskExecutor.submit( videoFrameProcessingTaskExecutor.submit(
@ -111,7 +112,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
videoFrameProcessingTaskExecutor.submit( videoFrameProcessingTaskExecutor.submit(
() -> { () -> {
if (pendingBitmaps.isEmpty()) { if (pendingBitmaps.isEmpty()) {
shaderProgram.signalEndOfCurrentInputStream(); checkNotNull(shaderProgram).signalEndOfCurrentInputStream();
DebugTraceUtil.logEvent( DebugTraceUtil.logEvent(
DebugTraceUtil.EVENT_BITMAP_TEXTURE_MANAGER_SIGNAL_EOS, C.TIME_END_OF_SOURCE); DebugTraceUtil.EVENT_BITMAP_TEXTURE_MANAGER_SIGNAL_EOS, C.TIME_END_OF_SOURCE);
} else { } else {
@ -166,8 +167,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
downstreamShaderProgramCapacity--; downstreamShaderProgramCapacity--;
shaderProgram.queueInputFrame( checkNotNull(shaderProgram)
glObjectsProvider, checkNotNull(currentGlTextureInfo), currentPresentationTimeUs); .queueInputFrame(
glObjectsProvider, checkNotNull(currentGlTextureInfo), currentPresentationTimeUs);
DebugTraceUtil.logEvent( DebugTraceUtil.logEvent(
DebugTraceUtil.EVENT_VFP_QUEUE_BITMAP, DebugTraceUtil.EVENT_VFP_QUEUE_BITMAP,
currentPresentationTimeUs, currentPresentationTimeUs,
@ -181,7 +183,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
finishedBitmapInfo.bitmap.recycle(); finishedBitmapInfo.bitmap.recycle();
if (pendingBitmaps.isEmpty() && currentInputStreamEnded) { if (pendingBitmaps.isEmpty() && currentInputStreamEnded) {
// Only signal end of stream after all pending bitmaps are processed. // Only signal end of stream after all pending bitmaps are processed.
shaderProgram.signalEndOfCurrentInputStream(); checkNotNull(shaderProgram).signalEndOfCurrentInputStream();
DebugTraceUtil.logEvent( DebugTraceUtil.logEvent(
DebugTraceUtil.EVENT_BITMAP_TEXTURE_MANAGER_SIGNAL_EOS, C.TIME_END_OF_SOURCE); DebugTraceUtil.EVENT_BITMAP_TEXTURE_MANAGER_SIGNAL_EOS, C.TIME_END_OF_SOURCE);
currentInputStreamEnded = false; currentInputStreamEnded = false;

View File

@ -254,26 +254,6 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
throws VideoFrameProcessingException { throws VideoFrameProcessingException {
// TODO(b/261188041) Add tests to verify the Listener is invoked on the given Executor. // TODO(b/261188041) Add tests to verify the Listener is invoked on the given Executor.
checkArgument(inputColorInfo.isDataSpaceValid());
checkArgument(inputColorInfo.colorTransfer != C.COLOR_TRANSFER_LINEAR);
checkArgument(outputColorInfo.isDataSpaceValid());
checkArgument(outputColorInfo.colorTransfer != C.COLOR_TRANSFER_LINEAR);
if (ColorInfo.isTransferHdr(inputColorInfo) || ColorInfo.isTransferHdr(outputColorInfo)) {
checkArgument(enableColorTransfers);
}
if (inputColorInfo.colorSpace != outputColorInfo.colorSpace
|| ColorInfo.isTransferHdr(inputColorInfo) != ColorInfo.isTransferHdr(outputColorInfo)) {
// OpenGL tone mapping is only implemented for BT2020 to BT709 and HDR to SDR (Gamma 2.2).
// Gamma 2.2 is used instead of SMPTE 170M for SDR, despite MediaFormat's
// COLOR_TRANSFER_SDR_VIDEO being defined as SMPTE 170M. This is to match
// other known tone-mapping behavior within the Android ecosystem.
checkArgument(inputColorInfo.colorSpace == C.COLOR_SPACE_BT2020);
checkArgument(outputColorInfo.colorSpace != C.COLOR_SPACE_BT2020);
checkArgument(ColorInfo.isTransferHdr(inputColorInfo));
checkArgument(outputColorInfo.colorTransfer == C.COLOR_TRANSFER_GAMMA_2_2);
}
boolean shouldShutdownExecutorService = executorService == null; boolean shouldShutdownExecutorService = executorService == null;
ExecutorService instanceExecutorService = ExecutorService instanceExecutorService =
executorService == null ? Util.newSingleThreadExecutor(THREAD_NAME) : executorService; executorService == null ? Util.newSingleThreadExecutor(THREAD_NAME) : executorService;
@ -340,6 +320,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
private final List<Effect> activeEffects; private final List<Effect> activeEffects;
private final Object lock; private final Object lock;
private final boolean enableColorTransfers;
private final ColorInfo firstInputColorInfo;
private final ColorInfo outputColorInfo; private final ColorInfo outputColorInfo;
private volatile @MonotonicNonNull FrameInfo nextInputFrameInfo; private volatile @MonotonicNonNull FrameInfo nextInputFrameInfo;
@ -356,6 +338,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
Executor listenerExecutor, Executor listenerExecutor,
FinalShaderProgramWrapper finalShaderProgramWrapper, FinalShaderProgramWrapper finalShaderProgramWrapper,
boolean renderFramesAutomatically, boolean renderFramesAutomatically,
boolean enableColorTransfers,
ColorInfo firstInputColorInfo,
ColorInfo outputColorInfo) { ColorInfo outputColorInfo) {
this.context = context; this.context = context;
this.glObjectsProvider = glObjectsProvider; this.glObjectsProvider = glObjectsProvider;
@ -368,6 +352,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
this.renderFramesAutomatically = renderFramesAutomatically; this.renderFramesAutomatically = renderFramesAutomatically;
this.activeEffects = new ArrayList<>(); this.activeEffects = new ArrayList<>();
this.lock = new Object(); this.lock = new Object();
this.enableColorTransfers = enableColorTransfers;
this.firstInputColorInfo = firstInputColorInfo;
this.outputColorInfo = outputColorInfo; this.outputColorInfo = outputColorInfo;
this.finalShaderProgramWrapper = finalShaderProgramWrapper; this.finalShaderProgramWrapper = finalShaderProgramWrapper;
this.intermediateGlShaderPrograms = new ArrayList<>(); this.intermediateGlShaderPrograms = new ArrayList<>();
@ -477,7 +463,10 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
synchronized (lock) { synchronized (lock) {
// An input stream is pending until its effects are configured. // An input stream is pending until its effects are configured.
InputStreamInfo pendingInputStreamInfo = new InputStreamInfo(inputType, effects, frameInfo); // TODO: b/307952514 - Move inputColorInfo's API from Factory.create into registerInputStream,
// and from StreamInfo into FrameInfo.
InputStreamInfo pendingInputStreamInfo =
new InputStreamInfo(inputType, effects, this.firstInputColorInfo, frameInfo);
if (!registeredFirstInputStream) { if (!registeredFirstInputStream) {
registeredFirstInputStream = true; registeredFirstInputStream = true;
inputStreamRegisteredCondition.close(); inputStreamRegisteredCondition.close();
@ -640,11 +629,6 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
: GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_8888; : GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_8888;
EGLContext eglContext = EGLContext eglContext =
createFocusedEglContextWithFallback(glObjectsProvider, eglDisplay, configAttributes); createFocusedEglContextWithFallback(glObjectsProvider, eglDisplay, configAttributes);
if ((ColorInfo.isTransferHdr(inputColorInfo) || ColorInfo.isTransferHdr(outputColorInfo))
&& GlUtil.getContextMajorVersion() != 3) {
throw new VideoFrameProcessingException(
"OpenGL ES 3.0 context support is required for HDR input or output.");
}
// Not renderFramesAutomatically means outputting to a display surface. HDR display surfaces // Not renderFramesAutomatically means outputting to a display surface. HDR display surfaces
// require the BT2020 PQ GL extension. // require the BT2020 PQ GL extension.
@ -689,16 +673,6 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
textureOutputListener, textureOutputListener,
textureOutputCapacity); textureOutputCapacity);
inputSwitcher.registerInput(inputColorInfo, INPUT_TYPE_SURFACE);
if (!ColorInfo.isTransferHdr(inputColorInfo)) {
// HDR bitmap input is not supported. Bitmaps are always sRGB/Full range/BT.709.
inputSwitcher.registerInput(ColorInfo.SRGB_BT709_FULL, INPUT_TYPE_BITMAP);
}
if (inputColorInfo.colorTransfer != C.COLOR_TRANSFER_SRGB) {
// Image and textureId concatenation not supported.
inputSwitcher.registerInput(inputColorInfo, INPUT_TYPE_TEXTURE_ID);
}
return new DefaultVideoFrameProcessor( return new DefaultVideoFrameProcessor(
context, context,
glObjectsProvider, glObjectsProvider,
@ -710,6 +684,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
videoFrameProcessorListenerExecutor, videoFrameProcessorListenerExecutor,
finalShaderProgramWrapper, finalShaderProgramWrapper,
renderFramesAutomatically, renderFramesAutomatically,
enableColorTransfers,
/* firstInputColorInfo= */ inputColorInfo,
outputColorInfo); outputColorInfo);
} }
@ -826,6 +802,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
*/ */
private void configureEffects(InputStreamInfo inputStreamInfo, boolean forceReconfigure) private void configureEffects(InputStreamInfo inputStreamInfo, boolean forceReconfigure)
throws VideoFrameProcessingException { throws VideoFrameProcessingException {
checkColors(firstInputColorInfo, outputColorInfo, enableColorTransfers);
if (forceReconfigure || !activeEffects.equals(inputStreamInfo.effects)) { if (forceReconfigure || !activeEffects.equals(inputStreamInfo.effects)) {
if (!intermediateGlShaderPrograms.isEmpty()) { if (!intermediateGlShaderPrograms.isEmpty()) {
for (int i = 0; i < intermediateGlShaderPrograms.size(); i++) { for (int i = 0; i < intermediateGlShaderPrograms.size(); i++) {
@ -853,7 +830,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
activeEffects.addAll(inputStreamInfo.effects); activeEffects.addAll(inputStreamInfo.effects);
} }
inputSwitcher.switchToInput(inputStreamInfo.inputType, inputStreamInfo.frameInfo); inputSwitcher.switchToInput(
inputStreamInfo.inputType, inputStreamInfo.frameInfo, inputStreamInfo.colorInfo);
inputStreamRegisteredCondition.open(); inputStreamRegisteredCondition.open();
listenerExecutor.execute( listenerExecutor.execute(
() -> () ->
@ -861,6 +839,42 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
inputStreamInfo.inputType, inputStreamInfo.effects, inputStreamInfo.frameInfo)); inputStreamInfo.inputType, inputStreamInfo.effects, inputStreamInfo.frameInfo));
} }
/** Checks that color configuration is valid for {@link DefaultVideoFrameProcessor}. */
private static void checkColors(
ColorInfo inputColorInfo, ColorInfo outputColorInfo, boolean enableColorTransfers)
throws VideoFrameProcessingException {
if ((ColorInfo.isTransferHdr(inputColorInfo) || ColorInfo.isTransferHdr(outputColorInfo))) {
checkArgument(enableColorTransfers);
long glVersion;
try {
glVersion = GlUtil.getContextMajorVersion();
} catch (GlUtil.GlException e) {
throw VideoFrameProcessingException.from(e);
}
if (glVersion != 3) {
throw new VideoFrameProcessingException(
"OpenGL ES 3.0 context support is required for HDR input or output.");
}
}
checkArgument(inputColorInfo.isDataSpaceValid());
checkArgument(inputColorInfo.colorTransfer != C.COLOR_TRANSFER_LINEAR);
checkArgument(outputColorInfo.isDataSpaceValid());
checkArgument(outputColorInfo.colorTransfer != C.COLOR_TRANSFER_LINEAR);
if (inputColorInfo.colorSpace != outputColorInfo.colorSpace
|| ColorInfo.isTransferHdr(inputColorInfo) != ColorInfo.isTransferHdr(outputColorInfo)) {
// OpenGL tone mapping is only implemented for BT2020 to BT709 and HDR to SDR (Gamma 2.2).
// Gamma 2.2 is used instead of SMPTE 170M for SDR, despite MediaFormat's
// COLOR_TRANSFER_SDR_VIDEO being defined as SMPTE 170M. This is to match
// other known tone-mapping behavior within the Android ecosystem.
checkArgument(inputColorInfo.colorSpace == C.COLOR_SPACE_BT2020);
checkArgument(outputColorInfo.colorSpace != C.COLOR_SPACE_BT2020);
checkArgument(ColorInfo.isTransferHdr(inputColorInfo));
checkArgument(outputColorInfo.colorTransfer == C.COLOR_TRANSFER_GAMMA_2_2);
}
}
/** /**
* Releases the {@link GlShaderProgram} instances and destroys the OpenGL context. * Releases the {@link GlShaderProgram} instances and destroys the OpenGL context.
* *
@ -925,11 +939,14 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
private static final class InputStreamInfo { private static final class InputStreamInfo {
public final @InputType int inputType; public final @InputType int inputType;
public final List<Effect> effects; public final List<Effect> effects;
public final ColorInfo colorInfo;
public final FrameInfo frameInfo; public final FrameInfo frameInfo;
public InputStreamInfo(@InputType int inputType, List<Effect> effects, FrameInfo frameInfo) { public InputStreamInfo(
@InputType int inputType, List<Effect> effects, ColorInfo colorInfo, FrameInfo frameInfo) {
this.inputType = inputType; this.inputType = inputType;
this.effects = effects; this.effects = effects;
this.colorInfo = colorInfo;
this.frameInfo = frameInfo; this.frameInfo = frameInfo;
} }
} }

View File

@ -15,6 +15,8 @@
*/ */
package androidx.media3.effect; package androidx.media3.effect;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.isRunningOnEmulator; import static androidx.media3.common.util.Util.isRunningOnEmulator;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
@ -30,12 +32,12 @@ import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.effect.GlShaderProgram.InputListener;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* Forwards externally produced frames that become available via a {@link SurfaceTexture} to an * Forwards externally produced frames that become available via a {@link SurfaceTexture} to an
@ -58,7 +60,7 @@ import java.util.concurrent.atomic.AtomicInteger;
private static final long SURFACE_TEXTURE_TIMEOUT_MS = isRunningOnEmulator() ? 10_000 : 500; private static final long SURFACE_TEXTURE_TIMEOUT_MS = isRunningOnEmulator() ? 10_000 : 500;
private final GlObjectsProvider glObjectsProvider; private final GlObjectsProvider glObjectsProvider;
private final ExternalShaderProgram externalShaderProgram; private @MonotonicNonNull ExternalShaderProgram externalShaderProgram;
private final int externalTexId; private final int externalTexId;
private final Surface surface; private final Surface surface;
private final SurfaceTexture surfaceTexture; private final SurfaceTexture surfaceTexture;
@ -82,8 +84,6 @@ import java.util.concurrent.atomic.AtomicInteger;
* Creates a new instance. The caller's thread must have a current GL context. * Creates a new instance. The caller's thread must have a current GL context.
* *
* @param glObjectsProvider The {@link GlObjectsProvider} for using EGL and GLES. * @param glObjectsProvider The {@link GlObjectsProvider} for using EGL and GLES.
* @param externalShaderProgram The {@link ExternalShaderProgram} for which this {@code
* ExternalTextureManager} will be set as the {@link InputListener}.
* @param videoFrameProcessingTaskExecutor The {@link VideoFrameProcessingTaskExecutor}. * @param videoFrameProcessingTaskExecutor The {@link VideoFrameProcessingTaskExecutor}.
* @throws VideoFrameProcessingException If a problem occurs while creating the external texture. * @throws VideoFrameProcessingException If a problem occurs while creating the external texture.
*/ */
@ -91,12 +91,10 @@ import java.util.concurrent.atomic.AtomicInteger;
@SuppressWarnings("nullness:method.invocation.invalid") @SuppressWarnings("nullness:method.invocation.invalid")
public ExternalTextureManager( public ExternalTextureManager(
GlObjectsProvider glObjectsProvider, GlObjectsProvider glObjectsProvider,
ExternalShaderProgram externalShaderProgram,
VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor) VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor)
throws VideoFrameProcessingException { throws VideoFrameProcessingException {
super(videoFrameProcessingTaskExecutor); super(videoFrameProcessingTaskExecutor);
this.glObjectsProvider = glObjectsProvider; this.glObjectsProvider = glObjectsProvider;
this.externalShaderProgram = externalShaderProgram;
try { try {
externalTexId = GlUtil.createExternalTexture(); externalTexId = GlUtil.createExternalTexture();
} catch (GlUtil.GlException e) { } catch (GlUtil.GlException e) {
@ -134,6 +132,17 @@ import java.util.concurrent.atomic.AtomicInteger;
surface = new Surface(surfaceTexture); surface = new Surface(surfaceTexture);
} }
/**
* {@inheritDoc}
*
* <p>{@code glShaderProgram} must be an {@link ExternalShaderProgram}.
*/
@Override
public void setSamplingGlShaderProgram(GlShaderProgram samplingGlShaderProgram) {
checkState(samplingGlShaderProgram instanceof ExternalShaderProgram);
this.externalShaderProgram = (ExternalShaderProgram) samplingGlShaderProgram;
}
@Override @Override
public void setDefaultBufferSize(int width, int height) { public void setDefaultBufferSize(int width, int height) {
surfaceTexture.setDefaultBufferSize(width, height); surfaceTexture.setDefaultBufferSize(width, height);
@ -161,7 +170,7 @@ import java.util.concurrent.atomic.AtomicInteger;
if (currentInputStreamEnded && pendingFrames.isEmpty()) { if (currentInputStreamEnded && pendingFrames.isEmpty()) {
// Reset because there could be further input streams after the current one ends. // Reset because there could be further input streams after the current one ends.
currentInputStreamEnded = false; currentInputStreamEnded = false;
externalShaderProgram.signalEndOfCurrentInputStream(); checkNotNull(externalShaderProgram).signalEndOfCurrentInputStream();
DebugTraceUtil.logEvent( DebugTraceUtil.logEvent(
DebugTraceUtil.EVENT_EXTERNAL_TEXTURE_MANAGER_SIGNAL_EOS, C.TIME_END_OF_SOURCE); DebugTraceUtil.EVENT_EXTERNAL_TEXTURE_MANAGER_SIGNAL_EOS, C.TIME_END_OF_SOURCE);
cancelForceSignalEndOfStreamTimer(); cancelForceSignalEndOfStreamTimer();
@ -200,7 +209,7 @@ import java.util.concurrent.atomic.AtomicInteger;
videoFrameProcessingTaskExecutor.submit( videoFrameProcessingTaskExecutor.submit(
() -> { () -> {
if (pendingFrames.isEmpty() && currentFrame == null) { if (pendingFrames.isEmpty() && currentFrame == null) {
externalShaderProgram.signalEndOfCurrentInputStream(); checkNotNull(externalShaderProgram).signalEndOfCurrentInputStream();
DebugTraceUtil.logEvent( DebugTraceUtil.logEvent(
DebugTraceUtil.EVENT_EXTERNAL_TEXTURE_MANAGER_SIGNAL_EOS, C.TIME_END_OF_SOURCE); DebugTraceUtil.EVENT_EXTERNAL_TEXTURE_MANAGER_SIGNAL_EOS, C.TIME_END_OF_SOURCE);
cancelForceSignalEndOfStreamTimer(); cancelForceSignalEndOfStreamTimer();
@ -294,20 +303,21 @@ import java.util.concurrent.atomic.AtomicInteger;
FrameInfo currentFrame = checkStateNotNull(this.currentFrame); FrameInfo currentFrame = checkStateNotNull(this.currentFrame);
externalShaderProgramInputCapacity.decrementAndGet(); externalShaderProgramInputCapacity.decrementAndGet();
surfaceTexture.getTransformMatrix(textureTransformMatrix); surfaceTexture.getTransformMatrix(textureTransformMatrix);
externalShaderProgram.setTextureTransformMatrix(textureTransformMatrix); checkNotNull(externalShaderProgram).setTextureTransformMatrix(textureTransformMatrix);
long frameTimeNs = surfaceTexture.getTimestamp(); long frameTimeNs = surfaceTexture.getTimestamp();
long offsetToAddUs = currentFrame.offsetToAddUs; long offsetToAddUs = currentFrame.offsetToAddUs;
// Correct presentationTimeUs so that GlShaderPrograms don't see the stream offset. // Correct presentationTimeUs so that GlShaderPrograms don't see the stream offset.
long presentationTimeUs = (frameTimeNs / 1000) + offsetToAddUs; long presentationTimeUs = (frameTimeNs / 1000) + offsetToAddUs;
externalShaderProgram.queueInputFrame( checkNotNull(externalShaderProgram)
glObjectsProvider, .queueInputFrame(
new GlTextureInfo( glObjectsProvider,
externalTexId, new GlTextureInfo(
/* fboId= */ C.INDEX_UNSET, externalTexId,
/* rboId= */ C.INDEX_UNSET, /* fboId= */ C.INDEX_UNSET,
currentFrame.width, /* rboId= */ C.INDEX_UNSET,
currentFrame.height), currentFrame.width,
presentationTimeUs); currentFrame.height),
presentationTimeUs);
checkStateNotNull(pendingFrames.remove()); checkStateNotNull(pendingFrames.remove());
DebugTraceUtil.logEvent(DebugTraceUtil.EVENT_VFP_QUEUE_FRAME, presentationTimeUs); DebugTraceUtil.logEvent(DebugTraceUtil.EVENT_VFP_QUEUE_FRAME, presentationTimeUs);
// If the queued frame is the last frame, end of stream will be signaled onInputFrameProcessed. // If the queued frame is the last frame, end of stream will be signaled onInputFrameProcessed.

View File

@ -28,6 +28,7 @@ import static androidx.media3.common.util.Util.contains;
import android.content.Context; import android.content.Context;
import android.util.SparseArray; import android.util.SparseArray;
import android.view.Surface; import android.view.Surface;
import androidx.media3.common.C;
import androidx.media3.common.ColorInfo; import androidx.media3.common.ColorInfo;
import androidx.media3.common.FrameInfo; import androidx.media3.common.FrameInfo;
import androidx.media3.common.GlObjectsProvider; import androidx.media3.common.GlObjectsProvider;
@ -38,6 +39,7 @@ import androidx.media3.common.VideoFrameProcessor;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/** /**
* A switcher to switch between {@linkplain TextureManager texture managers} of different * A switcher to switch between {@linkplain TextureManager texture managers} of different
@ -63,7 +65,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor, VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor,
Executor errorListenerExecutor, Executor errorListenerExecutor,
GlShaderProgram.ErrorListener samplingShaderProgramErrorListener, GlShaderProgram.ErrorListener samplingShaderProgramErrorListener,
boolean enableColorTransfers) { boolean enableColorTransfers)
throws VideoFrameProcessingException {
this.context = context; this.context = context;
this.outputColorInfo = outputColorInfo; this.outputColorInfo = outputColorInfo;
this.glObjectsProvider = glObjectsProvider; this.glObjectsProvider = glObjectsProvider;
@ -72,30 +75,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.samplingShaderProgramErrorListener = samplingShaderProgramErrorListener; this.samplingShaderProgramErrorListener = samplingShaderProgramErrorListener;
this.inputs = new SparseArray<>(); this.inputs = new SparseArray<>();
this.enableColorTransfers = enableColorTransfers; this.enableColorTransfers = enableColorTransfers;
// TODO(b/274109008): Investigate lazy instantiating the texture managers.
inputs.put(
INPUT_TYPE_SURFACE,
new Input(new ExternalTextureManager(glObjectsProvider, videoFrameProcessingTaskExecutor)));
inputs.put(
INPUT_TYPE_BITMAP,
new Input(new BitmapTextureManager(glObjectsProvider, videoFrameProcessingTaskExecutor)));
inputs.put(
INPUT_TYPE_TEXTURE_ID,
new Input(new TexIdTextureManager(glObjectsProvider, videoFrameProcessingTaskExecutor)));
} }
/** private DefaultShaderProgram createSamplingShaderProgram(
* Registers for a new {@link VideoFrameProcessor.InputType input}. ColorInfo inputColorInfo, @VideoFrameProcessor.InputType int inputType)
*
* <p>Can be called multiple times on the same {@link VideoFrameProcessor.InputType inputType},
* with the new inputs overwriting the old ones. For example, a new instance of {@link
* ExternalTextureManager} is created following each call to this method with {@link
* VideoFrameProcessor#INPUT_TYPE_SURFACE}. Effectively, the {@code inputSwitcher} keeps exactly
* one {@link TextureManager} per {@linkplain VideoFrameProcessor.InputType input type}.
*
* <p>Creates an {@link TextureManager} and an appropriate {@linkplain DefaultShaderProgram
* sampler} to sample from the input.
*
* @param inputColorInfo The {@link ColorInfo} for the input frames.
* @param inputType The {@linkplain VideoFrameProcessor.InputType type} of the input being
* registered.
*/
public void registerInput(ColorInfo inputColorInfo, @VideoFrameProcessor.InputType int inputType)
throws VideoFrameProcessingException { throws VideoFrameProcessingException {
// TODO(b/274109008): Investigate lazy instantiating the texture managers.
DefaultShaderProgram samplingShaderProgram;
TextureManager textureManager;
// TODO(b/274109008): Refactor DefaultShaderProgram to create a class just for sampling. // TODO(b/274109008): Refactor DefaultShaderProgram to create a class just for sampling.
DefaultShaderProgram samplingShaderProgram;
switch (inputType) { switch (inputType) {
case INPUT_TYPE_SURFACE: case INPUT_TYPE_SURFACE:
samplingShaderProgram = samplingShaderProgram =
@ -106,29 +103,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
inputColorInfo, inputColorInfo,
outputColorInfo, outputColorInfo,
enableColorTransfers); enableColorTransfers);
samplingShaderProgram.setErrorListener(
errorListenerExecutor, samplingShaderProgramErrorListener);
textureManager =
new ExternalTextureManager(
glObjectsProvider, samplingShaderProgram, videoFrameProcessingTaskExecutor);
break; break;
case INPUT_TYPE_BITMAP: case INPUT_TYPE_BITMAP:
// HDR bitmap input is not supported. Bitmaps are always sRGB/Full range/BT.709.
checkState(!ColorInfo.isTransferHdr(inputColorInfo));
ColorInfo bitmapColorInfo = ColorInfo.SRGB_BT709_FULL;
samplingShaderProgram = samplingShaderProgram =
DefaultShaderProgram.createWithInternalSampler( DefaultShaderProgram.createWithInternalSampler(
context, context,
/* matrixTransformations= */ ImmutableList.of(), /* matrixTransformations= */ ImmutableList.of(),
/* rgbMatrices= */ ImmutableList.of(), /* rgbMatrices= */ ImmutableList.of(),
inputColorInfo, bitmapColorInfo,
outputColorInfo, outputColorInfo,
enableColorTransfers, enableColorTransfers,
inputType); inputType);
samplingShaderProgram.setErrorListener(
errorListenerExecutor, samplingShaderProgramErrorListener);
textureManager =
new BitmapTextureManager(
glObjectsProvider, samplingShaderProgram, videoFrameProcessingTaskExecutor);
break; break;
case INPUT_TYPE_TEXTURE_ID: case INPUT_TYPE_TEXTURE_ID:
// Image and textureId concatenation not supported.
checkState(inputColorInfo.colorTransfer != C.COLOR_TRANSFER_SRGB);
samplingShaderProgram = samplingShaderProgram =
DefaultShaderProgram.createWithInternalSampler( DefaultShaderProgram.createWithInternalSampler(
context, context,
@ -138,16 +130,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
outputColorInfo, outputColorInfo,
enableColorTransfers, enableColorTransfers,
inputType); inputType);
samplingShaderProgram.setErrorListener(
errorListenerExecutor, samplingShaderProgramErrorListener);
textureManager =
new TexIdTextureManager(
glObjectsProvider, samplingShaderProgram, videoFrameProcessingTaskExecutor);
break; break;
default: default:
throw new VideoFrameProcessingException("Unsupported input type " + inputType); throw new VideoFrameProcessingException("Unsupported input type " + inputType);
} }
inputs.put(inputType, new Input(textureManager, samplingShaderProgram)); samplingShaderProgram.setErrorListener(
errorListenerExecutor, samplingShaderProgramErrorListener);
return samplingShaderProgram;
} }
/** Sets the {@link GlShaderProgram} that {@code InputSwitcher} outputs to. */ /** Sets the {@link GlShaderProgram} that {@code InputSwitcher} outputs to. */
@ -158,14 +147,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* Switches to a new source of input. * Switches to a new source of input.
* *
* <p>Must be called after the corresponding {@code newInputType} is {@linkplain #registerInput * <p>The first time this is called for each {@link VideoFrameProcessor.InputType}, a sampling
* registered}. * {@link GlShaderProgram} is created for the {@code newInputType}.
* *
* @param newInputType The new {@link VideoFrameProcessor.InputType} to switch to. * @param newInputType The new {@link VideoFrameProcessor.InputType} to switch to.
* @param inputFrameInfo The {@link FrameInfo} associated with the new input. * @param inputFrameInfo The {@link FrameInfo} associated with the new input.
* @param inputColorInfo The {@link ColorInfo} associated with the new input.
*/ */
public void switchToInput( public void switchToInput(
@VideoFrameProcessor.InputType int newInputType, FrameInfo inputFrameInfo) { @VideoFrameProcessor.InputType int newInputType,
FrameInfo inputFrameInfo,
ColorInfo inputColorInfo)
throws VideoFrameProcessingException {
checkStateNotNull(downstreamShaderProgram); checkStateNotNull(downstreamShaderProgram);
checkState(contains(inputs, newInputType), "Input type not registered: " + newInputType); checkState(contains(inputs, newInputType), "Input type not registered: " + newInputType);
@ -173,10 +166,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@VideoFrameProcessor.InputType int inputType = inputs.keyAt(i); @VideoFrameProcessor.InputType int inputType = inputs.keyAt(i);
Input input = inputs.get(inputType); Input input = inputs.get(inputType);
if (inputType == newInputType) { if (inputType == newInputType) {
if (input.getSamplingGlShaderProgram() == null) {
// TODO: b/307952514 - When switchToInput is called, and the inputColorInfo doesn't match
// the prior inputColorInfo, recreate and reinitialize the input.samplingGlShaderProgram.
input.setSamplingGlShaderProgram(
createSamplingShaderProgram(inputColorInfo, newInputType));
}
input.setChainingListener( input.setChainingListener(
new GatedChainingListenerWrapper( new GatedChainingListenerWrapper(
glObjectsProvider, glObjectsProvider,
input.samplingGlShaderProgram, checkNotNull(input.getSamplingGlShaderProgram()),
this.downstreamShaderProgram, this.downstreamShaderProgram,
videoFrameProcessingTaskExecutor)); videoFrameProcessingTaskExecutor));
input.setActive(true); input.setActive(true);
@ -217,31 +216,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* *
* @return The input {@link Surface}, regardless if the current input is {@linkplain * @return The input {@link Surface}, regardless if the current input is {@linkplain
* #switchToInput set} to {@link VideoFrameProcessor#INPUT_TYPE_SURFACE}. * #switchToInput set} to {@link VideoFrameProcessor#INPUT_TYPE_SURFACE}.
* @throws IllegalStateException If {@link VideoFrameProcessor#INPUT_TYPE_SURFACE} is not
* {@linkplain #registerInput registered}.
*/ */
public Surface getInputSurface() { public Surface getInputSurface() {
checkState(contains(inputs, INPUT_TYPE_SURFACE)); checkState(contains(inputs, INPUT_TYPE_SURFACE));
return inputs.get(INPUT_TYPE_SURFACE).textureManager.getInputSurface(); return inputs.get(INPUT_TYPE_SURFACE).textureManager.getInputSurface();
} }
/** /** See {@link DefaultVideoFrameProcessor#setInputDefaultBufferSize}. */
* See {@link DefaultVideoFrameProcessor#setInputDefaultBufferSize}.
*
* @throws IllegalStateException If {@link VideoFrameProcessor#INPUT_TYPE_SURFACE} is not
* {@linkplain #registerInput registered}.
*/
public void setInputDefaultBufferSize(int width, int height) { public void setInputDefaultBufferSize(int width, int height) {
checkState(contains(inputs, INPUT_TYPE_SURFACE)); checkState(contains(inputs, INPUT_TYPE_SURFACE));
inputs.get(INPUT_TYPE_SURFACE).textureManager.setDefaultBufferSize(width, height); inputs.get(INPUT_TYPE_SURFACE).textureManager.setDefaultBufferSize(width, height);
} }
/** /** Sets the {@link OnInputFrameProcessedListener}. */
* Sets the {@link OnInputFrameProcessedListener}.
*
* @throws IllegalStateException If {@link VideoFrameProcessor#INPUT_TYPE_TEXTURE_ID} is not
* {@linkplain #registerInput registered}.
*/
public void setOnInputFrameProcessedListener(OnInputFrameProcessedListener listener) { public void setOnInputFrameProcessedListener(OnInputFrameProcessedListener listener) {
checkState(contains(inputs, INPUT_TYPE_TEXTURE_ID)); checkState(contains(inputs, INPUT_TYPE_TEXTURE_ID));
inputs.get(INPUT_TYPE_TEXTURE_ID).textureManager.setOnInputFrameProcessedListener(listener); inputs.get(INPUT_TYPE_TEXTURE_ID).textureManager.setOnInputFrameProcessedListener(listener);
@ -262,19 +249,27 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/ */
private static final class Input { private static final class Input {
public final TextureManager textureManager; public final TextureManager textureManager;
public final GlShaderProgram samplingGlShaderProgram;
private @MonotonicNonNull ExternalShaderProgram samplingGlShaderProgram;
private @MonotonicNonNull GatedChainingListenerWrapper gatedChainingListenerWrapper; private @MonotonicNonNull GatedChainingListenerWrapper gatedChainingListenerWrapper;
public Input(TextureManager textureManager, GlShaderProgram samplingGlShaderProgram) { public Input(TextureManager textureManager) {
this.textureManager = textureManager; this.textureManager = textureManager;
}
public void setSamplingGlShaderProgram(ExternalShaderProgram samplingGlShaderProgram) {
this.samplingGlShaderProgram = samplingGlShaderProgram; this.samplingGlShaderProgram = samplingGlShaderProgram;
textureManager.setSamplingGlShaderProgram(samplingGlShaderProgram);
samplingGlShaderProgram.setInputListener(textureManager); samplingGlShaderProgram.setInputListener(textureManager);
} }
public void setChainingListener(GatedChainingListenerWrapper gatedChainingListenerWrapper) { public void setChainingListener(GatedChainingListenerWrapper gatedChainingListenerWrapper) {
this.gatedChainingListenerWrapper = gatedChainingListenerWrapper; this.gatedChainingListenerWrapper = gatedChainingListenerWrapper;
samplingGlShaderProgram.setOutputListener(gatedChainingListenerWrapper); checkNotNull(samplingGlShaderProgram).setOutputListener(gatedChainingListenerWrapper);
}
public @Nullable ExternalShaderProgram getSamplingGlShaderProgram() {
return samplingGlShaderProgram;
} }
public void setActive(boolean active) { public void setActive(boolean active) {
@ -286,7 +281,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public void release() throws VideoFrameProcessingException { public void release() throws VideoFrameProcessingException {
textureManager.release(); textureManager.release();
samplingGlShaderProgram.release(); if (samplingGlShaderProgram != null) {
samplingGlShaderProgram.release();
}
} }
} }

View File

@ -31,31 +31,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* a {@link GlShaderProgram} for consumption. * a {@link GlShaderProgram} for consumption.
*/ */
/* package */ final class TexIdTextureManager extends TextureManager { /* package */ final class TexIdTextureManager extends TextureManager {
private final FrameConsumptionManager frameConsumptionManager; private @MonotonicNonNull FrameConsumptionManager frameConsumptionManager;
private @MonotonicNonNull OnInputFrameProcessedListener frameProcessedListener; private @MonotonicNonNull OnInputFrameProcessedListener frameProcessedListener;
private @MonotonicNonNull FrameInfo inputFrameInfo; private @MonotonicNonNull FrameInfo inputFrameInfo;
private final GlObjectsProvider glObjectsProvider;
/** /**
* Creates a new instance. * Creates a new instance.
* *
* @param glObjectsProvider The {@link GlObjectsProvider} for using EGL and GLES. * @param glObjectsProvider The {@link GlObjectsProvider} for using EGL and GLES.
* @param shaderProgram The {@link GlShaderProgram} for which this {@code texIdTextureManager}
* will be set as the {@link GlShaderProgram.InputListener}.
* @param videoFrameProcessingTaskExecutor The {@link VideoFrameProcessingTaskExecutor}. * @param videoFrameProcessingTaskExecutor The {@link VideoFrameProcessingTaskExecutor}.
*/ */
public TexIdTextureManager( public TexIdTextureManager(
GlObjectsProvider glObjectsProvider, GlObjectsProvider glObjectsProvider,
GlShaderProgram shaderProgram,
VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor) { VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor) {
super(videoFrameProcessingTaskExecutor); super(videoFrameProcessingTaskExecutor);
frameConsumptionManager = this.glObjectsProvider = glObjectsProvider;
new FrameConsumptionManager(
glObjectsProvider, shaderProgram, videoFrameProcessingTaskExecutor);
} }
@Override @Override
public void onReadyToAcceptInputFrame() { public void onReadyToAcceptInputFrame() {
checkNotNull(frameConsumptionManager);
videoFrameProcessingTaskExecutor.submit(frameConsumptionManager::onReadyToAcceptInputFrame); videoFrameProcessingTaskExecutor.submit(frameConsumptionManager::onReadyToAcceptInputFrame);
} }
@ -67,6 +64,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
.onInputFrameProcessed(inputTexture.texId, GlUtil.createGlSyncFence())); .onInputFrameProcessed(inputTexture.texId, GlUtil.createGlSyncFence()));
} }
@Override
public void setSamplingGlShaderProgram(GlShaderProgram samplingGlShaderProgram) {
frameConsumptionManager =
new FrameConsumptionManager(
glObjectsProvider, samplingGlShaderProgram, videoFrameProcessingTaskExecutor);
}
@Override @Override
public void queueInputTexture(int inputTexId, long presentationTimeUs) { public void queueInputTexture(int inputTexId, long presentationTimeUs) {
FrameInfo frameInfo = checkNotNull(this.inputFrameInfo); FrameInfo frameInfo = checkNotNull(this.inputFrameInfo);
@ -80,7 +84,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* rboId= */ C.INDEX_UNSET, /* rboId= */ C.INDEX_UNSET,
frameInfo.width, frameInfo.width,
frameInfo.height); frameInfo.height);
frameConsumptionManager.queueInputFrame(inputTexture, presentationTimeUs); checkNotNull(frameConsumptionManager).queueInputFrame(inputTexture, presentationTimeUs);
DebugTraceUtil.logEvent( DebugTraceUtil.logEvent(
DebugTraceUtil.EVENT_VFP_QUEUE_TEXTURE, DebugTraceUtil.EVENT_VFP_QUEUE_TEXTURE,
presentationTimeUs, presentationTimeUs,
@ -102,14 +106,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public int getPendingFrameCount() { public int getPendingFrameCount() {
return frameConsumptionManager.getPendingFrameCount(); return checkNotNull(frameConsumptionManager).getPendingFrameCount();
} }
@Override @Override
public void signalEndOfCurrentInputStream() { public void signalEndOfCurrentInputStream() {
videoFrameProcessingTaskExecutor.submit( videoFrameProcessingTaskExecutor.submit(
() -> { () -> {
frameConsumptionManager.signalEndOfCurrentStream(); checkNotNull(frameConsumptionManager).signalEndOfCurrentStream();
DebugTraceUtil.logEvent( DebugTraceUtil.logEvent(
DebugTraceUtil.EVENT_TEX_ID_TEXTURE_MANAGER_SIGNAL_EOS, C.TIME_END_OF_SOURCE); DebugTraceUtil.EVENT_TEX_ID_TEXTURE_MANAGER_SIGNAL_EOS, C.TIME_END_OF_SOURCE);
}); });
@ -124,7 +128,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
protected synchronized void flush() { protected synchronized void flush() {
frameConsumptionManager.onFlush(); checkNotNull(frameConsumptionManager).onFlush();
super.flush(); super.flush();
} }
} }

View File

@ -64,6 +64,16 @@ import androidx.media3.common.util.TimestampIterator;
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
/**
* Sets the {@link GlShaderProgram} that consumes the {@link TextureManager}'s output.
*
* <p>Must be called before any method that queues input or {@link
* #signalEndOfCurrentInputStream()}.
*
* <p>This must only be called once.
*/
public abstract void setSamplingGlShaderProgram(GlShaderProgram samplingGlShaderProgram);
/** /**
* Provides an input {@link Bitmap} to put into the video frames. * Provides an input {@link Bitmap} to put into the video frames.
* *