Add frame cache to support replay frames

PiperOrigin-RevId: 737727035
This commit is contained in:
claincly 2025-03-17 13:14:38 -07:00 committed by Copybara-Service
parent 059cb23f3d
commit 9ac58fa405
9 changed files with 354 additions and 56 deletions

View File

@ -280,6 +280,8 @@ public interface VideoFrameProcessor {
/** /**
* Updates an {@linkplain Listener#onOutputFrameAvailableForRendering available frame} with the * Updates an {@linkplain Listener#onOutputFrameAvailableForRendering available frame} with the
* modified effects. * modified effects.
*
* <p>This method can be called from any thread.
*/ */
void redraw(); void redraw();

View File

@ -20,6 +20,7 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
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 static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.GlUtil.getDefaultEglDisplay; import static androidx.media3.common.util.GlUtil.getDefaultEglDisplay;
import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.effect.DebugTraceUtil.COMPONENT_VFP; import static androidx.media3.effect.DebugTraceUtil.COMPONENT_VFP;
import static androidx.media3.effect.DebugTraceUtil.EVENT_RECEIVE_END_OF_ALL_INPUT; import static androidx.media3.effect.DebugTraceUtil.EVENT_RECEIVE_END_OF_ALL_INPUT;
import static androidx.media3.effect.DebugTraceUtil.EVENT_REGISTER_NEW_INPUT_STREAM; import static androidx.media3.effect.DebugTraceUtil.EVENT_REGISTER_NEW_INPUT_STREAM;
@ -157,6 +158,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
private @MonotonicNonNull GlObjectsProvider glObjectsProvider; private @MonotonicNonNull GlObjectsProvider glObjectsProvider;
private GlTextureProducer.@MonotonicNonNull Listener textureOutputListener; private GlTextureProducer.@MonotonicNonNull Listener textureOutputListener;
private int textureOutputCapacity; private int textureOutputCapacity;
private boolean enableReplayableCache;
private boolean requireRegisteringAllInputFrames; private boolean requireRegisteringAllInputFrames;
private boolean experimentalAdjustSurfaceTextureTransformationMatrix; private boolean experimentalAdjustSurfaceTextureTransformationMatrix;
private boolean experimentalRepeatInputBitmapWithoutResampling; private boolean experimentalRepeatInputBitmapWithoutResampling;
@ -175,6 +177,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
glObjectsProvider = factory.glObjectsProvider; glObjectsProvider = factory.glObjectsProvider;
textureOutputListener = factory.textureOutputListener; textureOutputListener = factory.textureOutputListener;
textureOutputCapacity = factory.textureOutputCapacity; textureOutputCapacity = factory.textureOutputCapacity;
enableReplayableCache = factory.enableReplayableCache;
requireRegisteringAllInputFrames = !factory.repeatLastRegisteredFrame; requireRegisteringAllInputFrames = !factory.repeatLastRegisteredFrame;
experimentalAdjustSurfaceTextureTransformationMatrix = experimentalAdjustSurfaceTextureTransformationMatrix =
factory.experimentalAdjustSurfaceTextureTransformationMatrix; factory.experimentalAdjustSurfaceTextureTransformationMatrix;
@ -265,6 +268,22 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
return this; return this;
} }
/**
* Sets whether to use a frame cache to {@link DefaultVideoFrameProcessor#redraw} frames.
*
* <p>The default value is {@code false}, in this case calling {@link
* VideoFrameProcessor#redraw} throws {@link UnsupportedOperationException}.
*
* <p>Using a frame cache enables precise redrawing, but increases resource and power usages.
*
* @param enableReplayableCache Whether to use a frame cache.
*/
@CanIgnoreReturnValue
public Builder setEnableReplayableCache(boolean enableReplayableCache) {
this.enableReplayableCache = enableReplayableCache;
return this;
}
/** /**
* Sets texture output settings. * Sets texture output settings.
* *
@ -340,6 +359,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
executorService, executorService,
textureOutputListener, textureOutputListener,
textureOutputCapacity, textureOutputCapacity,
enableReplayableCache,
experimentalAdjustSurfaceTextureTransformationMatrix, experimentalAdjustSurfaceTextureTransformationMatrix,
experimentalRepeatInputBitmapWithoutResampling); experimentalRepeatInputBitmapWithoutResampling);
} }
@ -351,6 +371,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
@Nullable private final ExecutorService executorService; @Nullable private final ExecutorService executorService;
@Nullable private final GlTextureProducer.Listener textureOutputListener; @Nullable private final GlTextureProducer.Listener textureOutputListener;
private final int textureOutputCapacity; private final int textureOutputCapacity;
private final boolean enableReplayableCache;
private final boolean experimentalAdjustSurfaceTextureTransformationMatrix; private final boolean experimentalAdjustSurfaceTextureTransformationMatrix;
private final boolean experimentalRepeatInputBitmapWithoutResampling; private final boolean experimentalRepeatInputBitmapWithoutResampling;
@ -361,6 +382,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
@Nullable ExecutorService executorService, @Nullable ExecutorService executorService,
@Nullable GlTextureProducer.Listener textureOutputListener, @Nullable GlTextureProducer.Listener textureOutputListener,
int textureOutputCapacity, int textureOutputCapacity,
boolean enableReplayableCache,
boolean experimentalAdjustSurfaceTextureTransformationMatrix, boolean experimentalAdjustSurfaceTextureTransformationMatrix,
boolean experimentalRepeatInputBitmapWithoutResampling) { boolean experimentalRepeatInputBitmapWithoutResampling) {
this.sdrWorkingColorSpace = sdrWorkingColorSpace; this.sdrWorkingColorSpace = sdrWorkingColorSpace;
@ -369,6 +391,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
this.executorService = executorService; this.executorService = executorService;
this.textureOutputListener = textureOutputListener; this.textureOutputListener = textureOutputListener;
this.textureOutputCapacity = textureOutputCapacity; this.textureOutputCapacity = textureOutputCapacity;
this.enableReplayableCache = enableReplayableCache;
this.experimentalAdjustSurfaceTextureTransformationMatrix = this.experimentalAdjustSurfaceTextureTransformationMatrix =
experimentalAdjustSurfaceTextureTransformationMatrix; experimentalAdjustSurfaceTextureTransformationMatrix;
this.experimentalRepeatInputBitmapWithoutResampling = this.experimentalRepeatInputBitmapWithoutResampling =
@ -437,6 +460,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
listener, listener,
instanceGlObjectsProvider, instanceGlObjectsProvider,
shouldReleaseGlObjectsProvider, shouldReleaseGlObjectsProvider,
enableReplayableCache,
textureOutputListener, textureOutputListener,
textureOutputCapacity, textureOutputCapacity,
repeatLastRegisteredFrame, repeatLastRegisteredFrame,
@ -492,6 +516,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
private final Object lock; private final Object lock;
private final ColorInfo outputColorInfo; private final ColorInfo outputColorInfo;
private final DebugViewProvider debugViewProvider; private final DebugViewProvider debugViewProvider;
@Nullable private final ReplayableFrameCacheGlShaderProgram frameCache;
private volatile @MonotonicNonNull FrameInfo nextInputFrameInfo; private volatile @MonotonicNonNull FrameInfo nextInputFrameInfo;
private volatile boolean inputStreamEnded; private volatile boolean inputStreamEnded;
@ -508,7 +533,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
FinalShaderProgramWrapper finalShaderProgramWrapper, FinalShaderProgramWrapper finalShaderProgramWrapper,
boolean renderFramesAutomatically, boolean renderFramesAutomatically,
ColorInfo outputColorInfo, ColorInfo outputColorInfo,
DebugViewProvider debugViewProvider) { DebugViewProvider debugViewProvider,
@Nullable ReplayableFrameCacheGlShaderProgram frameCache) {
this.context = context; this.context = context;
this.glObjectsProvider = glObjectsProvider; this.glObjectsProvider = glObjectsProvider;
this.shouldReleaseGlObjectsProvider = shouldReleaseGlObjectsProvider; this.shouldReleaseGlObjectsProvider = shouldReleaseGlObjectsProvider;
@ -521,18 +547,30 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
this.activeEffects = new ArrayList<>(); this.activeEffects = new ArrayList<>();
this.lock = new Object(); this.lock = new Object();
this.outputColorInfo = outputColorInfo; this.outputColorInfo = outputColorInfo;
this.frameCache = frameCache;
this.debugViewProvider = debugViewProvider; this.debugViewProvider = debugViewProvider;
this.finalShaderProgramWrapper = finalShaderProgramWrapper; this.finalShaderProgramWrapper = finalShaderProgramWrapper;
this.intermediateGlShaderPrograms = new ArrayList<>(); this.intermediateGlShaderPrograms = new ArrayList<>();
this.inputStreamRegisteredCondition = new ConditionVariable(); this.inputStreamRegisteredCondition = new ConditionVariable();
inputStreamRegisteredCondition.open(); inputStreamRegisteredCondition.open();
this.finalShaderProgramWrapper.setListener( this.finalShaderProgramWrapper.setListener(
() -> { new FinalShaderProgramWrapper.Listener() {
@Override
public void onInputStreamProcessed() {
if (inputStreamEnded) { if (inputStreamEnded) {
listenerExecutor.execute(listener::onEnded); listenerExecutor.execute(listener::onEnded);
DebugTraceUtil.logEvent(COMPONENT_VFP, EVENT_SIGNAL_ENDED, C.TIME_END_OF_SOURCE); DebugTraceUtil.logEvent(COMPONENT_VFP, EVENT_SIGNAL_ENDED, C.TIME_END_OF_SOURCE);
} else { } else {
submitPendingInputStream(); DefaultVideoFrameProcessor.this.submitPendingInputStream();
}
}
@Override
public void onFrameRendered(long presentationTimeUs) {
if (frameCache == null) {
return;
}
frameCache.onFrameRendered(presentationTimeUs);
} }
}); });
} }
@ -624,9 +662,31 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
return inputSwitcher.getInputSurface(); return inputSwitcher.getInputSurface();
} }
/**
* {@inheritDoc}
*
* <p>{@code DefaultVideoFrameProcessor} keeps track of the redraw requests received. If a call to
* redraw is made when another redraw request is ongoing, the new request will be performed later
* when the ongoing redraw completes, and this method will return immediately.
*/
@Override @Override
public void redraw() { public void redraw() {
throw new UnsupportedOperationException(); if (frameCache == null) {
throw new UnsupportedOperationException(
"Replaying when enableReplayableCache is set to false");
}
// TODO: b/391109644 - Call listener method in VideoFrameMetadataListener and debounce
// accordingly.
if (frameCache.isEmpty()) {
// Don't redraw right after flush, because the frame cache is also be flushed and it's empty.
return;
}
videoFrameProcessingTaskExecutor.submit(
() -> {
finalShaderProgramWrapper.prepareToRedraw(
castNonNull(frameCache).getReplayFramePresentationTimeUs());
frameCache.replayFrame();
});
} }
/** /**
@ -853,6 +913,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
Listener listener, Listener listener,
GlObjectsProvider glObjectsProvider, GlObjectsProvider glObjectsProvider,
boolean shouldReleaseGlObjectsProvider, boolean shouldReleaseGlObjectsProvider,
boolean enableReplayableCache,
@Nullable GlTextureProducer.Listener textureOutputListener, @Nullable GlTextureProducer.Listener textureOutputListener,
int textureOutputCapacity, int textureOutputCapacity,
boolean repeatLastRegisteredFrame, boolean repeatLastRegisteredFrame,
@ -860,8 +921,9 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
boolean experimentalRepeatInputBitmapWithoutResampling) boolean experimentalRepeatInputBitmapWithoutResampling)
throws GlUtil.GlException, VideoFrameProcessingException { throws GlUtil.GlException, VideoFrameProcessingException {
EGLDisplay eglDisplay = getDefaultEglDisplay(); EGLDisplay eglDisplay = getDefaultEglDisplay();
boolean isOutputTransferHdr = ColorInfo.isTransferHdr(outputColorInfo);
int[] configAttributes = int[] configAttributes =
ColorInfo.isTransferHdr(outputColorInfo) isOutputTransferHdr
? GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_1010102 ? GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_1010102
: GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_8888; : GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_8888;
Pair<EGLContext, EGLSurface> eglContextAndPlaceholderSurface = Pair<EGLContext, EGLSurface> eglContextAndPlaceholderSurface =
@ -874,7 +936,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
.setHdrStaticInfo(null) .setHdrStaticInfo(null)
.build(); .build();
ColorInfo intermediateColorInfo = ColorInfo intermediateColorInfo =
ColorInfo.isTransferHdr(outputColorInfo) isOutputTransferHdr
? linearColorInfo ? linearColorInfo
: sdrWorkingColorSpace == WORKING_COLOR_SPACE_LINEAR : sdrWorkingColorSpace == WORKING_COLOR_SPACE_LINEAR
? linearColorInfo ? linearColorInfo
@ -919,7 +981,10 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
finalShaderProgramWrapper, finalShaderProgramWrapper,
renderFramesAutomatically, renderFramesAutomatically,
outputColorInfo, outputColorInfo,
debugViewProvider); debugViewProvider,
enableReplayableCache
? new ReplayableFrameCacheGlShaderProgram(context, /* useHdr= */ isOutputTransferHdr)
: null);
} }
/** /**
@ -1042,18 +1107,25 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
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++) { // If frameCache is present, it's the first item in the list, skip releasing it.
int startIndex = frameCache == null ? 0 : 1;
for (int i = startIndex; i < intermediateGlShaderPrograms.size(); i++) {
intermediateGlShaderPrograms.get(i).release(); intermediateGlShaderPrograms.get(i).release();
} }
intermediateGlShaderPrograms.clear(); intermediateGlShaderPrograms.clear();
} }
if (frameCache != null) {
intermediateGlShaderPrograms.add(frameCache);
}
ImmutableList.Builder<Effect> effectsListBuilder = ImmutableList.Builder<Effect> effectsListBuilder =
new ImmutableList.Builder<Effect>().addAll(inputStreamInfo.effects); new ImmutableList.Builder<Effect>().addAll(inputStreamInfo.effects);
if (debugViewProvider != DebugViewProvider.NONE) { if (debugViewProvider != DebugViewProvider.NONE) {
effectsListBuilder.add(new DebugViewEffect(debugViewProvider, outputColorInfo)); effectsListBuilder.add(new DebugViewEffect(debugViewProvider, outputColorInfo));
} }
// The GlShaderPrograms that should be inserted in between InputSwitcher and
// The GlShaderPrograms that should be inserted in between the frame cache and
// FinalShaderProgramWrapper. // FinalShaderProgramWrapper.
intermediateGlShaderPrograms.addAll( intermediateGlShaderPrograms.addAll(
createGlShaderPrograms( createGlShaderPrograms(

View File

@ -41,6 +41,7 @@ import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.LongArrayQueue; import androidx.media3.common.util.LongArrayQueue;
import androidx.media3.common.util.Size; import androidx.media3.common.util.Size;
import androidx.media3.common.util.SystemClock;
import androidx.media3.effect.DefaultVideoFrameProcessor.WorkingColorSpace; import androidx.media3.effect.DefaultVideoFrameProcessor.WorkingColorSpace;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.ArrayList; import java.util.ArrayList;
@ -72,6 +73,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* #signalEndOfCurrentInputStream()}. * #signalEndOfCurrentInputStream()}.
*/ */
void onInputStreamProcessed(); void onInputStreamProcessed();
/** Called when a frame is rendered to the output surface. */
void onFrameRendered(long presentationTimeUs);
} }
private static final String TAG = "FinalShaderWrapper"; private static final String TAG = "FinalShaderWrapper";
@ -112,6 +116,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private boolean outputSurfaceInfoChanged; private boolean outputSurfaceInfoChanged;
@Nullable private SurfaceInfo outputSurfaceInfo; @Nullable private SurfaceInfo outputSurfaceInfo;
private long redrawFramePresentationTimeUs;
/** Wraps the {@link Surface} in {@link #outputSurfaceInfo}. */ /** Wraps the {@link Surface} in {@link #outputSurfaceInfo}. */
@Nullable private EGLSurface outputEglSurface; @Nullable private EGLSurface outputEglSurface;
@ -149,6 +155,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
outputTexturePool = new TexturePool(useHighPrecisionColorComponents, textureOutputCapacity); outputTexturePool = new TexturePool(useHighPrecisionColorComponents, textureOutputCapacity);
outputTextureTimestamps = new LongArrayQueue(textureOutputCapacity); outputTextureTimestamps = new LongArrayQueue(textureOutputCapacity);
syncObjects = new LongArrayQueue(textureOutputCapacity); syncObjects = new LongArrayQueue(textureOutputCapacity);
redrawFramePresentationTimeUs = C.TIME_UNSET;
} }
// GlTextureProducer interface. Can be called on any thread. // GlTextureProducer interface. Can be called on any thread.
@ -213,8 +220,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public void queueInputFrame( public void queueInputFrame(
GlObjectsProvider glObjectsProvider, GlTextureInfo inputTexture, long presentationTimeUs) { GlObjectsProvider glObjectsProvider, GlTextureInfo inputTexture, long presentationTimeUs) {
videoFrameProcessingTaskExecutor.verifyVideoFrameProcessingThread(); videoFrameProcessingTaskExecutor.verifyVideoFrameProcessingThread();
if (!isWaitingForRedrawFrame()) {
// Don't report output available when redrawing - the redrawn frames are released immediately.
videoFrameProcessorListenerExecutor.execute( videoFrameProcessorListenerExecutor.execute(
() -> videoFrameProcessorListener.onOutputFrameAvailableForRendering(presentationTimeUs)); () -> videoFrameProcessorListener.onOutputFrameAvailableForRendering(presentationTimeUs));
}
if (textureOutputListener == null) { if (textureOutputListener == null) {
if (renderFramesAutomatically) { if (renderFramesAutomatically) {
renderFrame( renderFrame(
@ -224,6 +235,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* renderTimeNs= */ presentationTimeUs * 1000); /* renderTimeNs= */ presentationTimeUs * 1000);
} else { } else {
availableFrames.add(new TimedGlTextureInfo(inputTexture, presentationTimeUs)); availableFrames.add(new TimedGlTextureInfo(inputTexture, presentationTimeUs));
if (isWaitingForRedrawFrame()) {
if (presentationTimeUs == redrawFramePresentationTimeUs) {
redrawFramePresentationTimeUs = C.TIME_UNSET;
renderFrame(
glObjectsProvider,
inputTexture,
presentationTimeUs,
/* renderTimeNs= */ SystemClock.DEFAULT.nanoTime());
availableFrames.clear();
} else {
// Skip other frames when waiting for the replay frame to arrive, so that the producer
// can continue processing, but keep it in the availableFrames for the player to call
// renderFrame.
inputListener.onInputFrameProcessed(inputTexture);
}
}
} }
inputListener.onReadyToAcceptInputFrame(); inputListener.onReadyToAcceptInputFrame();
} else { } else {
@ -309,6 +336,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return; return;
} }
checkState(!renderFramesAutomatically); checkState(!renderFramesAutomatically);
if (availableFrames.isEmpty()) {
// This only happens with redrawn frame. The available output frame notification on the player
// side runs on another thread and when redrawing rapidly, the player could receive an output
// frame from a previous redraw.
return;
}
TimedGlTextureInfo oldestAvailableFrame = availableFrames.remove(); TimedGlTextureInfo oldestAvailableFrame = availableFrames.remove();
renderFrame( renderFrame(
glObjectsProvider, glObjectsProvider,
@ -337,6 +371,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
/* package */ void prepareToRedraw(long redrawFramePresentationTimeUs) {
this.redrawFramePresentationTimeUs = redrawFramePresentationTimeUs;
for (int i = 0; i < availableFrames.size(); i++) {
TimedGlTextureInfo availableFrame = availableFrames.remove();
inputListener.onInputFrameProcessed(availableFrame.glTextureInfo);
}
}
/** Must be called on the GL thread. */ /** Must be called on the GL thread. */
private void setOutputSurfaceInfoInternal(@Nullable SurfaceInfo outputSurfaceInfo) { private void setOutputSurfaceInfoInternal(@Nullable SurfaceInfo outputSurfaceInfo) {
if (textureOutputListener != null) { if (textureOutputListener != null) {
@ -391,6 +433,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
private boolean isWaitingForRedrawFrame() {
return redrawFramePresentationTimeUs != C.TIME_UNSET;
}
private void renderFrame( private void renderFrame(
GlObjectsProvider glObjectsProvider, GlObjectsProvider glObjectsProvider,
GlTextureInfo inputTexture, GlTextureInfo inputTexture,
@ -398,8 +444,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
long renderTimeNs) { long renderTimeNs) {
try { try {
if (renderTimeNs == VideoFrameProcessor.DROP_OUTPUT_FRAME if (renderTimeNs == VideoFrameProcessor.DROP_OUTPUT_FRAME
|| !ensureConfigured(glObjectsProvider, inputTexture.width, inputTexture.height)) { || !ensureConfigured(glObjectsProvider, inputTexture.width, inputTexture.height)
|| (isWaitingForRedrawFrame() && presentationTimeUs != redrawFramePresentationTimeUs)) {
inputListener.onInputFrameProcessed(inputTexture); inputListener.onInputFrameProcessed(inputTexture);
if (renderTimeNs == VideoFrameProcessor.DROP_OUTPUT_FRAME) {
checkNotNull(listener).onFrameRendered(presentationTimeUs);
}
return; // Drop frames when requested, or there is no output surface and output texture. return; // Drop frames when requested, or there is no output surface and output texture.
} }
if (outputSurfaceInfo != null) { if (outputSurfaceInfo != null) {
@ -445,6 +495,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
EGLExt.eglPresentationTimeANDROID(eglDisplay, outputEglSurface, eglPresentationTimeNs); EGLExt.eglPresentationTimeANDROID(eglDisplay, outputEglSurface, eglPresentationTimeNs);
EGL14.eglSwapBuffers(eglDisplay, outputEglSurface); EGL14.eglSwapBuffers(eglDisplay, outputEglSurface);
checkNotNull(listener).onFrameRendered(presentationTimeUs);
DebugTraceUtil.logEvent(COMPONENT_VFP, EVENT_RENDERED_TO_OUTPUT_SURFACE, presentationTimeUs); DebugTraceUtil.logEvent(COMPONENT_VFP, EVENT_RENDERED_TO_OUTPUT_SURFACE, presentationTimeUs);
} }

View File

@ -0,0 +1,124 @@
/*
* Copyright 2025 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.effect;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import android.content.Context;
import androidx.media3.common.C;
import androidx.media3.common.GlObjectsProvider;
import androidx.media3.common.GlTextureInfo;
import androidx.media3.common.VideoFrameProcessingException;
/**
* A shader program that caches the input frames, and {@linkplain #replayFrame replays} the oldest
* input frame when instructed.
*/
/* package */ final class ReplayableFrameCacheGlShaderProgram extends FrameCacheGlShaderProgram {
private static final int CAPACITY = 2;
private static final int REPLAY_FRAME_INDEX = 0;
private static final int REGULAR_FRAME_INDEX = 1;
// Use a manually managed array to be more efficient than List add/remove methods.
private final TimedGlTextureInfo[] cachedFrames;
private int cacheSize;
public ReplayableFrameCacheGlShaderProgram(Context context, boolean useHdr)
throws VideoFrameProcessingException {
super(context, CAPACITY, useHdr);
cachedFrames = new TimedGlTextureInfo[CAPACITY];
}
@Override
public void queueInputFrame(
GlObjectsProvider glObjectsProvider, GlTextureInfo inputTexture, long presentationTimeUs) {
checkState(cacheSize < CAPACITY);
super.queueInputFrame(glObjectsProvider, inputTexture, presentationTimeUs);
cachedFrames[cacheSize++] =
new TimedGlTextureInfo(
checkNotNull(outputTexturePool.getMostRecentlyUsedTexture()), presentationTimeUs);
}
@Override
public void releaseOutputFrame(GlTextureInfo outputTexture) {
// Do nothing here as this method will be called as soon as the output frame is queued into the
// subsequent shader program. This class only releases output frame based on rendering event
// from the FinalShaderProgramWrapper. See onFrameRendered().
}
@Override
public void flush() {
cacheSize = 0;
super.flush();
}
/** Returns whether there is no cached frame. */
public boolean isEmpty() {
return cacheSize == 0;
}
/**
* Returns the presentation time of the frame that will be replayed, if {@link #replayFrame()} is
* called.
*/
public long getReplayFramePresentationTimeUs() {
if (isEmpty()) {
return C.TIME_UNSET;
}
return cachedFrames[REPLAY_FRAME_INDEX].presentationTimeUs;
}
/**
* Replays the frame from cache, with the {@linkplain #getReplayFramePresentationTimeUs replay
* timestamp}.
*/
public void replayFrame() {
if (isEmpty()) {
return;
}
// Get the oldest frame that is queued.
TimedGlTextureInfo oldestFrame = cachedFrames[REPLAY_FRAME_INDEX];
getOutputListener()
.onOutputFrameAvailable(oldestFrame.glTextureInfo, oldestFrame.presentationTimeUs);
// Queue the subsequent frame also to keep the player's output frame queue full.
if (cacheSize > 1) {
TimedGlTextureInfo secondOldestFrame = cachedFrames[REGULAR_FRAME_INDEX];
getOutputListener()
.onOutputFrameAvailable(
secondOldestFrame.glTextureInfo, secondOldestFrame.presentationTimeUs);
}
}
/** Removes a frame from the cache when a frame of the {@code presentationTimeUs} is rendered. */
public void onFrameRendered(long presentationTimeUs) {
// Cache needs to be full when capacity is two, only release frame n when frame n+1 is released.
if (cacheSize < CAPACITY
|| presentationTimeUs < cachedFrames[REGULAR_FRAME_INDEX].presentationTimeUs) {
return;
}
// Evict the oldest frame.
TimedGlTextureInfo cachedFrame = cachedFrames[REPLAY_FRAME_INDEX];
cachedFrames[REPLAY_FRAME_INDEX] = cachedFrames[REGULAR_FRAME_INDEX];
cacheSize--;
// Release the texture, this also calls readyToAcceptInput.
super.releaseOutputFrame(cachedFrame.glTextureInfo);
}
}

View File

@ -278,7 +278,7 @@ public class SingleInputVideoGraph implements VideoGraph {
@Override @Override
public void redraw() { public void redraw() {
throw new UnsupportedOperationException(); checkStateNotNull(videoFrameProcessor).redraw();
} }
@Override @Override

View File

@ -17,18 +17,19 @@ package androidx.media3.effect;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import androidx.annotation.Nullable;
import androidx.media3.common.GlObjectsProvider; import androidx.media3.common.GlObjectsProvider;
import androidx.media3.common.GlTextureInfo; import androidx.media3.common.GlTextureInfo;
import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.GlUtil;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator; import java.util.Iterator;
import java.util.Queue;
/** Holds {@code capacity} textures, to re-use textures. */ /** Holds {@code capacity} textures, to re-use textures. */
/* package */ final class TexturePool { /* package */ final class TexturePool {
private final Queue<GlTextureInfo> freeTextures; private final Deque<GlTextureInfo> freeTextures;
private final Queue<GlTextureInfo> inUseTextures; private final Deque<GlTextureInfo> inUseTextures;
private final int capacity; private final int capacity;
private final boolean useHighPrecisionColorComponents; private final boolean useHighPrecisionColorComponents;
@ -94,6 +95,15 @@ import java.util.Queue;
return texture; return texture;
} }
/** Returns the {@link GlTextureInfo} that is most recently {@linkplain #useTexture used}. */
@Nullable
public GlTextureInfo getMostRecentlyUsedTexture() {
if (inUseTextures.isEmpty()) {
return null;
}
return inUseTextures.getLast();
}
/** /**
* Frees the texture represented by {@code textureInfo}. * Frees the texture represented by {@code textureInfo}.
* *

View File

@ -78,4 +78,5 @@
-dontnote androidx.media3.effect.DefaultVideoFrameProcessor$Factory$Builder -dontnote androidx.media3.effect.DefaultVideoFrameProcessor$Factory$Builder
-keepclasseswithmembers class androidx.media3.effect.DefaultVideoFrameProcessor$Factory$Builder { -keepclasseswithmembers class androidx.media3.effect.DefaultVideoFrameProcessor$Factory$Builder {
androidx.media3.effect.DefaultVideoFrameProcessor$Factory build(); androidx.media3.effect.DefaultVideoFrameProcessor$Factory build();
androidx.media3.effect.DefaultVideoFrameProcessor$Factory$Builder setEnableReplayableCache(boolean);
} }

View File

@ -870,9 +870,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
if (!hasSetVideoSink) { if (!hasSetVideoSink) {
if (videoEffects != null && videoSink == null) { if (videoEffects != null && videoSink == null) {
PlaybackVideoGraphWrapper playbackVideoGraphWrapper = PlaybackVideoGraphWrapper playbackVideoGraphWrapper =
new PlaybackVideoGraphWrapper.Builder(context, videoFrameReleaseControl) createPlaybackVideoGraphWrapper(context, videoFrameReleaseControl);
.setClock(getClock())
.build();
playbackVideoGraphWrapper.setTotalVideoInputCount(1); playbackVideoGraphWrapper.setTotalVideoInputCount(1);
videoSink = playbackVideoGraphWrapper.getSink(/* inputIndex= */ 0); videoSink = playbackVideoGraphWrapper.getSink(/* inputIndex= */ 0);
} }
@ -946,6 +944,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
} }
} }
/** Creates a {@link PlaybackVideoGraphWrapper} instance. */
protected PlaybackVideoGraphWrapper createPlaybackVideoGraphWrapper(
Context context, VideoFrameReleaseControl videoFrameReleaseControl) {
// TODO: b/391109644 - Add a more explicit API to enable replaying.
return new PlaybackVideoGraphWrapper.Builder(context, videoFrameReleaseControl)
.setClock(getClock())
.build();
}
@Override @Override
public void enableMayRenderStartOfStream() { public void enableMayRenderStartOfStream() {
if (videoSink != null) { if (videoSink != null) {

View File

@ -62,6 +62,7 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@ -128,6 +129,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
private Clock clock; private Clock clock;
private boolean requestOpenGlToneMapping; private boolean requestOpenGlToneMapping;
private boolean built; private boolean built;
private boolean enableReplayableCache;
/** Creates a builder. */ /** Creates a builder. */
public Builder(Context context, VideoFrameReleaseControl videoFrameReleaseControl) { public Builder(Context context, VideoFrameReleaseControl videoFrameReleaseControl) {
@ -222,6 +224,21 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
return this; return this;
} }
/**
* Sets whether to enable replayable cache.
*
* <p>By default, the replayable cache is not enabled. Enable it to achieve accurate effect
* update, at the cost of using more power and computing resources.
*
* @param enableReplayableCache Whether replayable cache is enabled.
* @return This builder, for convenience.
*/
@CanIgnoreReturnValue
public Builder setEnableReplayableCache(boolean enableReplayableCache) {
this.enableReplayableCache = enableReplayableCache;
return this;
}
/** /**
* Builds the {@link PlaybackVideoGraphWrapper}. * Builds the {@link PlaybackVideoGraphWrapper}.
* *
@ -233,7 +250,8 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
if (videoGraphFactory == null) { if (videoGraphFactory == null) {
if (videoFrameProcessorFactory == null) { if (videoFrameProcessorFactory == null) {
videoFrameProcessorFactory = new ReflectiveDefaultVideoFrameProcessorFactory(); videoFrameProcessorFactory =
new ReflectiveDefaultVideoFrameProcessorFactory(enableReplayableCache);
} }
videoGraphFactory = new ReflectiveSingleInputVideoGraphFactory(videoFrameProcessorFactory); videoGraphFactory = new ReflectiveSingleInputVideoGraphFactory(videoFrameProcessorFactory);
} }
@ -696,6 +714,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
@Override @Override
public void redraw() { public void redraw() {
checkState(isInitialized()); checkState(isInitialized());
PlaybackVideoGraphWrapper.this.flush(/* resetPosition= */ false);
checkNotNull(videoGraph).redraw(); checkNotNull(videoGraph).redraw();
} }
@ -1059,30 +1078,26 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
*/ */
private static final class ReflectiveDefaultVideoFrameProcessorFactory private static final class ReflectiveDefaultVideoFrameProcessorFactory
implements VideoFrameProcessor.Factory { implements VideoFrameProcessor.Factory {
private static final Supplier<VideoFrameProcessor.Factory>
VIDEO_FRAME_PROCESSOR_FACTORY_SUPPLIER = private static final Supplier<Class<?>> DEFAULT_VIDEO_FRAME_PROCESSOR_FACTORY_BUILDER_CLASS =
Suppliers.memoize( Suppliers.memoize(
() -> { () -> {
try { try {
// LINT.IfChange // LINT.IfChange
Class<?> defaultVideoFrameProcessorFactoryBuilderClass = return Class.forName(
Class.forName(
"androidx.media3.effect.DefaultVideoFrameProcessor$Factory$Builder"); "androidx.media3.effect.DefaultVideoFrameProcessor$Factory$Builder");
Object builder =
defaultVideoFrameProcessorFactoryBuilderClass
.getConstructor()
.newInstance();
return (VideoFrameProcessor.Factory)
checkNotNull(
defaultVideoFrameProcessorFactoryBuilderClass
.getMethod("build")
.invoke(builder));
// LINT.ThenChange(../../../../../../../proguard-rules.txt) // LINT.ThenChange(../../../../../../../proguard-rules.txt)
} catch (Exception e) { } catch (Exception e) {
throw new IllegalStateException(e); throw new IllegalStateException(e);
} }
}); });
private final boolean enableReplayableCache;
public ReflectiveDefaultVideoFrameProcessorFactory(boolean enableReplayableCache) {
this.enableReplayableCache = enableReplayableCache;
}
@Override @Override
public VideoFrameProcessor create( public VideoFrameProcessor create(
Context context, Context context,
@ -1092,15 +1107,31 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
Executor listenerExecutor, Executor listenerExecutor,
VideoFrameProcessor.Listener listener) VideoFrameProcessor.Listener listener)
throws VideoFrameProcessingException { throws VideoFrameProcessingException {
return VIDEO_FRAME_PROCESSOR_FACTORY_SUPPLIER try {
.get() Class<?> defaultVideoFrameProcessorFactoryBuilderClass =
.create( DEFAULT_VIDEO_FRAME_PROCESSOR_FACTORY_BUILDER_CLASS.get();
Object builder =
defaultVideoFrameProcessorFactoryBuilderClass.getConstructor().newInstance();
Method setUseReplayableCacheMethod =
defaultVideoFrameProcessorFactoryBuilderClass.getMethod(
"setEnableReplayableCache", boolean.class);
setUseReplayableCacheMethod.invoke(builder, enableReplayableCache);
VideoFrameProcessor.Factory factory =
(VideoFrameProcessor.Factory)
checkNotNull(
defaultVideoFrameProcessorFactoryBuilderClass
.getMethod("build")
.invoke(builder));
return factory.create(
context, context,
debugViewProvider, debugViewProvider,
outputColorInfo, outputColorInfo,
renderFramesAutomatically, renderFramesAutomatically,
listenerExecutor, listenerExecutor,
listener); listener);
} catch (Exception e) {
throw new VideoFrameProcessingException(e);
}
} }
} }
} }