mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Support chaining async GlTextureProcessors in FrameProcessorChain.
After this change, FrameProcessorChain chains any GlTextureProcessors instead of only SingleFrameGlTextureProcessors. The GlTextureProcessors are chained in a bidirectional manner using ChainingGlTextureProcessorListener to feed input and output related events forward and release events backwards. PiperOrigin-RevId: 456478414
This commit is contained in:
parent
a7649b639c
commit
555ab97e34
@ -17,6 +17,7 @@ package androidx.media3.demo.transformer;
|
|||||||
|
|
||||||
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -420,9 +421,28 @@ public final class TransformerActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
private final class DemoDebugViewProvider implements Transformer.DebugViewProvider {
|
private final class DemoDebugViewProvider implements Transformer.DebugViewProvider {
|
||||||
|
|
||||||
|
private @MonotonicNonNull SurfaceView surfaceView;
|
||||||
|
private int width;
|
||||||
|
private int height;
|
||||||
|
|
||||||
|
public DemoDebugViewProvider() {
|
||||||
|
width = C.LENGTH_UNSET;
|
||||||
|
height = C.LENGTH_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public SurfaceView getDebugPreviewSurfaceView(int width, int height) {
|
public SurfaceView getDebugPreviewSurfaceView(int width, int height) {
|
||||||
|
checkState(
|
||||||
|
surfaceView == null || (this.width == width && this.height == height),
|
||||||
|
"Transformer should not change the output size mid-transformation.");
|
||||||
|
if (surfaceView != null) {
|
||||||
|
return surfaceView;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
|
||||||
// Update the UI on the main thread and wait for the output surface to be available.
|
// Update the UI on the main thread and wait for the output surface to be available.
|
||||||
CountDownLatch surfaceCreatedCountDownLatch = new CountDownLatch(1);
|
CountDownLatch surfaceCreatedCountDownLatch = new CountDownLatch(1);
|
||||||
SurfaceView surfaceView = new SurfaceView(/* context= */ TransformerActivity.this);
|
SurfaceView surfaceView = new SurfaceView(/* context= */ TransformerActivity.this);
|
||||||
@ -459,6 +479,7 @@ public final class TransformerActivity extends AppCompatActivity {
|
|||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
this.surfaceView = surfaceView;
|
||||||
return surfaceView;
|
return surfaceView;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,342 @@
|
|||||||
|
/*
|
||||||
|
* 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.checkState;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.opengl.EGL14;
|
||||||
|
import android.opengl.EGLContext;
|
||||||
|
import android.opengl.EGLDisplay;
|
||||||
|
import android.opengl.EGLExt;
|
||||||
|
import android.opengl.EGLSurface;
|
||||||
|
import android.util.Size;
|
||||||
|
import android.view.Surface;
|
||||||
|
import android.view.SurfaceHolder;
|
||||||
|
import android.view.SurfaceView;
|
||||||
|
import androidx.annotation.GuardedBy;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
import androidx.media3.common.C;
|
||||||
|
import androidx.media3.common.util.GlUtil;
|
||||||
|
import androidx.media3.common.util.Log;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around a {@link GlTextureProcessor} that writes to the provided output surface and
|
||||||
|
* optional debug surface view.
|
||||||
|
*
|
||||||
|
* <p>The wrapped {@link GlTextureProcessor} applies the {@link GlMatrixTransformation} instances
|
||||||
|
* passed to the constructor, followed by any transformations needed to convert the frames to the
|
||||||
|
* dimensions specified by the provided {@link SurfaceInfo}.
|
||||||
|
*
|
||||||
|
* <p>This wrapper is used for the final {@link GlTextureProcessor} instance in the chain of {@link
|
||||||
|
* GlTextureProcessor} instances used by {@link FrameProcessorChain}.
|
||||||
|
*/
|
||||||
|
/* package */ final class FinalMatrixTransformationProcessorWrapper implements GlTextureProcessor {
|
||||||
|
|
||||||
|
private static final String TAG = "FinalProcessorWrapper";
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final ImmutableList<GlMatrixTransformation> matrixTransformations;
|
||||||
|
private final EGLDisplay eglDisplay;
|
||||||
|
private final EGLContext eglContext;
|
||||||
|
private final SurfaceInfo.Provider outputSurfaceProvider;
|
||||||
|
private final long streamOffsetUs;
|
||||||
|
private final Transformer.DebugViewProvider debugViewProvider;
|
||||||
|
private final FrameProcessorChain.Listener frameProcessorChainListener;
|
||||||
|
private final boolean enableExperimentalHdrEditing;
|
||||||
|
|
||||||
|
private int inputWidth;
|
||||||
|
private int inputHeight;
|
||||||
|
@Nullable private MatrixTransformationProcessor matrixTransformationProcessor;
|
||||||
|
@Nullable private SurfaceInfo outputSurfaceInfo;
|
||||||
|
@Nullable private EGLSurface outputEglSurface;
|
||||||
|
@Nullable private SurfaceViewWrapper debugSurfaceViewWrapper;
|
||||||
|
private @MonotonicNonNull Listener listener;
|
||||||
|
|
||||||
|
public FinalMatrixTransformationProcessorWrapper(
|
||||||
|
Context context,
|
||||||
|
EGLDisplay eglDisplay,
|
||||||
|
EGLContext eglContext,
|
||||||
|
ImmutableList<GlMatrixTransformation> matrixTransformations,
|
||||||
|
SurfaceInfo.Provider outputSurfaceProvider,
|
||||||
|
long streamOffsetUs,
|
||||||
|
FrameProcessorChain.Listener listener,
|
||||||
|
Transformer.DebugViewProvider debugViewProvider,
|
||||||
|
boolean enableExperimentalHdrEditing) {
|
||||||
|
this.context = context;
|
||||||
|
this.matrixTransformations = matrixTransformations;
|
||||||
|
this.eglDisplay = eglDisplay;
|
||||||
|
this.eglContext = eglContext;
|
||||||
|
this.outputSurfaceProvider = outputSurfaceProvider;
|
||||||
|
this.streamOffsetUs = streamOffsetUs;
|
||||||
|
this.debugViewProvider = debugViewProvider;
|
||||||
|
this.frameProcessorChainListener = listener;
|
||||||
|
this.enableExperimentalHdrEditing = enableExperimentalHdrEditing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The {@code FinalMatrixTransformationProcessorWrapper} will only call {@link
|
||||||
|
* Listener#onInputFrameProcessed(TextureInfo)}. Other events are handled via the {@link
|
||||||
|
* FrameProcessorChain.Listener} passed to the constructor.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setListener(Listener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean maybeQueueInputFrame(TextureInfo inputTexture, long presentationTimeUs) {
|
||||||
|
try {
|
||||||
|
if (!ensureConfigured(inputTexture.width, inputTexture.height)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
EGLSurface outputEglSurface = this.outputEglSurface;
|
||||||
|
SurfaceInfo outputSurfaceInfo = this.outputSurfaceInfo;
|
||||||
|
MatrixTransformationProcessor matrixTransformationProcessor =
|
||||||
|
this.matrixTransformationProcessor;
|
||||||
|
|
||||||
|
GlUtil.focusEglSurface(
|
||||||
|
eglDisplay,
|
||||||
|
eglContext,
|
||||||
|
outputEglSurface,
|
||||||
|
outputSurfaceInfo.width,
|
||||||
|
outputSurfaceInfo.height);
|
||||||
|
GlUtil.clearOutputFrame();
|
||||||
|
matrixTransformationProcessor.drawFrame(inputTexture.texId, presentationTimeUs);
|
||||||
|
EGLExt.eglPresentationTimeANDROID(
|
||||||
|
eglDisplay,
|
||||||
|
outputEglSurface,
|
||||||
|
/* presentationTimeNs= */ (presentationTimeUs + streamOffsetUs) * 1000);
|
||||||
|
EGL14.eglSwapBuffers(eglDisplay, outputEglSurface);
|
||||||
|
} catch (FrameProcessingException | GlUtil.GlException e) {
|
||||||
|
frameProcessorChainListener.onFrameProcessingError(
|
||||||
|
FrameProcessingException.from(e, presentationTimeUs));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debugSurfaceViewWrapper != null && matrixTransformationProcessor != null) {
|
||||||
|
MatrixTransformationProcessor matrixTransformationProcessor =
|
||||||
|
this.matrixTransformationProcessor;
|
||||||
|
try {
|
||||||
|
debugSurfaceViewWrapper.maybeRenderToSurfaceView(
|
||||||
|
() -> {
|
||||||
|
GlUtil.clearOutputFrame();
|
||||||
|
matrixTransformationProcessor.drawFrame(inputTexture.texId, presentationTimeUs);
|
||||||
|
});
|
||||||
|
} catch (FrameProcessingException | GlUtil.GlException e) {
|
||||||
|
Log.d(TAG, "Error rendering to debug preview", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onInputFrameProcessed(inputTexture);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EnsuresNonNullIf(
|
||||||
|
expression = {"outputSurfaceInfo", "outputEglSurface", "matrixTransformationProcessor"},
|
||||||
|
result = true)
|
||||||
|
private boolean ensureConfigured(int inputWidth, int inputHeight)
|
||||||
|
throws FrameProcessingException, GlUtil.GlException {
|
||||||
|
if (inputWidth == this.inputWidth
|
||||||
|
&& inputHeight == this.inputHeight
|
||||||
|
&& outputSurfaceInfo != null
|
||||||
|
&& outputEglSurface != null
|
||||||
|
&& matrixTransformationProcessor != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inputWidth = inputWidth;
|
||||||
|
this.inputHeight = inputHeight;
|
||||||
|
Size requestedOutputSize =
|
||||||
|
MatrixUtils.configureAndGetOutputSize(inputWidth, inputHeight, matrixTransformations);
|
||||||
|
@Nullable
|
||||||
|
SurfaceInfo outputSurfaceInfo =
|
||||||
|
outputSurfaceProvider.getSurfaceInfo(
|
||||||
|
requestedOutputSize.getWidth(), requestedOutputSize.getHeight());
|
||||||
|
if (outputSurfaceInfo == null) {
|
||||||
|
if (matrixTransformationProcessor != null) {
|
||||||
|
matrixTransformationProcessor.release();
|
||||||
|
matrixTransformationProcessor = null;
|
||||||
|
}
|
||||||
|
outputEglSurface = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (outputSurfaceInfo == this.outputSurfaceInfo
|
||||||
|
&& outputEglSurface != null
|
||||||
|
&& matrixTransformationProcessor != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
EGLSurface outputEglSurface;
|
||||||
|
if (enableExperimentalHdrEditing) {
|
||||||
|
// TODO(b/227624622): Don't assume BT.2020 PQ input/output.
|
||||||
|
outputEglSurface = GlUtil.getEglSurfaceBt2020Pq(eglDisplay, outputSurfaceInfo.surface);
|
||||||
|
} else {
|
||||||
|
outputEglSurface = GlUtil.getEglSurface(eglDisplay, outputSurfaceInfo.surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
SurfaceView debugSurfaceView =
|
||||||
|
debugViewProvider.getDebugPreviewSurfaceView(
|
||||||
|
outputSurfaceInfo.width, outputSurfaceInfo.height);
|
||||||
|
if (debugSurfaceView != null) {
|
||||||
|
debugSurfaceViewWrapper =
|
||||||
|
new SurfaceViewWrapper(
|
||||||
|
eglDisplay, eglContext, enableExperimentalHdrEditing, debugSurfaceView);
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixTransformationProcessor =
|
||||||
|
createMatrixTransformationProcessorForOutputSurface(requestedOutputSize, outputSurfaceInfo);
|
||||||
|
|
||||||
|
this.outputSurfaceInfo = outputSurfaceInfo;
|
||||||
|
this.outputEglSurface = outputEglSurface;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MatrixTransformationProcessor createMatrixTransformationProcessorForOutputSurface(
|
||||||
|
Size requestedOutputSize, SurfaceInfo outputSurfaceInfo) throws FrameProcessingException {
|
||||||
|
ImmutableList.Builder<GlMatrixTransformation> matrixTransformationListBuilder =
|
||||||
|
new ImmutableList.Builder<GlMatrixTransformation>().addAll(matrixTransformations);
|
||||||
|
if (outputSurfaceInfo.orientationDegrees != 0) {
|
||||||
|
matrixTransformationListBuilder.add(
|
||||||
|
new ScaleToFitTransformation.Builder()
|
||||||
|
.setRotationDegrees(outputSurfaceInfo.orientationDegrees)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
if (outputSurfaceInfo.width != requestedOutputSize.getWidth()
|
||||||
|
|| outputSurfaceInfo.height != requestedOutputSize.getHeight()) {
|
||||||
|
matrixTransformationListBuilder.add(
|
||||||
|
Presentation.createForWidthAndHeight(
|
||||||
|
outputSurfaceInfo.width, outputSurfaceInfo.height, Presentation.LAYOUT_SCALE_TO_FIT));
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixTransformationProcessor matrixTransformationProcessor =
|
||||||
|
new MatrixTransformationProcessor(context, matrixTransformationListBuilder.build());
|
||||||
|
Size outputSize = matrixTransformationProcessor.configure(inputWidth, inputHeight);
|
||||||
|
checkState(outputSize.getWidth() == outputSurfaceInfo.width);
|
||||||
|
checkState(outputSize.getHeight() == outputSurfaceInfo.height);
|
||||||
|
return matrixTransformationProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void releaseOutputFrame(TextureInfo outputTexture) {
|
||||||
|
throw new UnsupportedOperationException(
|
||||||
|
"The final texture processor writes to a surface so there is no texture to release");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void signalEndOfInputStream() {
|
||||||
|
frameProcessorChainListener.onFrameProcessingEnded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@WorkerThread
|
||||||
|
public void release() throws FrameProcessingException {
|
||||||
|
if (matrixTransformationProcessor != null) {
|
||||||
|
matrixTransformationProcessor.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around a {@link SurfaceView} that keeps track of whether the output surface is valid,
|
||||||
|
* and makes rendering a no-op if not.
|
||||||
|
*/
|
||||||
|
private static final class SurfaceViewWrapper implements SurfaceHolder.Callback {
|
||||||
|
private final EGLDisplay eglDisplay;
|
||||||
|
private final EGLContext eglContext;
|
||||||
|
private final boolean enableExperimentalHdrEditing;
|
||||||
|
|
||||||
|
@GuardedBy("this")
|
||||||
|
@Nullable
|
||||||
|
private Surface surface;
|
||||||
|
|
||||||
|
@GuardedBy("this")
|
||||||
|
@Nullable
|
||||||
|
private EGLSurface eglSurface;
|
||||||
|
|
||||||
|
private int width;
|
||||||
|
private int height;
|
||||||
|
|
||||||
|
public SurfaceViewWrapper(
|
||||||
|
EGLDisplay eglDisplay,
|
||||||
|
EGLContext eglContext,
|
||||||
|
boolean enableExperimentalHdrEditing,
|
||||||
|
SurfaceView surfaceView) {
|
||||||
|
this.eglDisplay = eglDisplay;
|
||||||
|
this.eglContext = eglContext;
|
||||||
|
this.enableExperimentalHdrEditing = enableExperimentalHdrEditing;
|
||||||
|
surfaceView.getHolder().addCallback(this);
|
||||||
|
surface = surfaceView.getHolder().getSurface();
|
||||||
|
width = surfaceView.getWidth();
|
||||||
|
height = surfaceView.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focuses the wrapped surface view's surface as an {@link EGLSurface}, renders using {@code
|
||||||
|
* renderingTask} and swaps buffers, if the view's holder has a valid surface. Does nothing
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
public synchronized void maybeRenderToSurfaceView(FrameProcessingTask renderingTask)
|
||||||
|
throws GlUtil.GlException, FrameProcessingException {
|
||||||
|
if (surface == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eglSurface == null) {
|
||||||
|
if (enableExperimentalHdrEditing) {
|
||||||
|
eglSurface = GlUtil.getEglSurfaceBt2020Pq(eglDisplay, surface);
|
||||||
|
} else {
|
||||||
|
eglSurface = GlUtil.getEglSurface(eglDisplay, surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EGLSurface eglSurface = this.eglSurface;
|
||||||
|
GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, width, height);
|
||||||
|
renderingTask.run();
|
||||||
|
EGL14.eglSwapBuffers(eglDisplay, eglSurface);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void surfaceCreated(SurfaceHolder holder) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void surfaceChanged(
|
||||||
|
SurfaceHolder holder, int format, int width, int height) {
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
Surface newSurface = holder.getSurface();
|
||||||
|
if (surface == null || !surface.equals(newSurface)) {
|
||||||
|
surface = newSurface;
|
||||||
|
eglSurface = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void surfaceDestroyed(SurfaceHolder holder) {
|
||||||
|
surface = null;
|
||||||
|
eglSurface = null;
|
||||||
|
width = C.LENGTH_UNSET;
|
||||||
|
height = C.LENGTH_UNSET;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,35 +17,24 @@ package androidx.media3.transformer;
|
|||||||
|
|
||||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
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 com.google.common.collect.Iterables.getLast;
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.SurfaceTexture;
|
import android.graphics.SurfaceTexture;
|
||||||
import android.opengl.EGL14;
|
import android.opengl.EGL14;
|
||||||
import android.opengl.EGLContext;
|
import android.opengl.EGLContext;
|
||||||
import android.opengl.EGLDisplay;
|
import android.opengl.EGLDisplay;
|
||||||
import android.opengl.EGLExt;
|
|
||||||
import android.opengl.EGLSurface;
|
|
||||||
import android.util.Size;
|
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import android.view.SurfaceHolder;
|
|
||||||
import android.view.SurfaceView;
|
|
||||||
import androidx.annotation.GuardedBy;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.util.GlUtil;
|
import androidx.media3.common.util.GlUtil;
|
||||||
import androidx.media3.common.util.Log;
|
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import com.google.common.base.Optional;
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
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.AtomicBoolean;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@code FrameProcessorChain} applies changes to individual video frames.
|
* {@code FrameProcessorChain} applies changes to individual video frames.
|
||||||
@ -53,10 +42,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
* <p>Input becomes available on its {@linkplain #getInputSurface() input surface} asynchronously
|
* <p>Input becomes available on its {@linkplain #getInputSurface() input surface} asynchronously
|
||||||
* and is processed on a background thread as it becomes available. All input frames should be
|
* and is processed on a background thread as it becomes available. All input frames should be
|
||||||
* {@linkplain #registerInputFrame() registered} before they are rendered to the input surface.
|
* {@linkplain #registerInputFrame() registered} before they are rendered to the input surface.
|
||||||
* {@link #getPendingFrameCount()} can be used to check whether there are frames that have not been
|
* {@link #getPendingInputFrameCount()} can be used to check whether there are frames that have not
|
||||||
* fully processed yet. Output is written to the provided {@linkplain #create(Context, Listener,
|
* been fully processed yet. Output is written to the provided {@linkplain #create(Context,
|
||||||
* float, int, int, long, List, SurfaceInfo.Provider, Transformer.DebugViewProvider, boolean) output
|
* Listener, float, int, int, long, List, SurfaceInfo.Provider, Transformer.DebugViewProvider,
|
||||||
* surface}.
|
* boolean) output surface}.
|
||||||
*/
|
*/
|
||||||
// TODO(b/227625423): Factor out FrameProcessor interface and rename this class to GlFrameProcessor.
|
// TODO(b/227625423): Factor out FrameProcessor interface and rename this class to GlFrameProcessor.
|
||||||
/* package */ final class FrameProcessorChain {
|
/* package */ final class FrameProcessorChain {
|
||||||
@ -93,17 +82,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
* Surface}.
|
* Surface}.
|
||||||
* @param debugViewProvider A {@link Transformer.DebugViewProvider}.
|
* @param debugViewProvider A {@link Transformer.DebugViewProvider}.
|
||||||
* @param enableExperimentalHdrEditing Whether to attempt to process the input as an HDR signal.
|
* @param enableExperimentalHdrEditing Whether to attempt to process the input as an HDR signal.
|
||||||
* @return A new instance or {@code null}, if no output surface was provided.
|
* @return A new instance.
|
||||||
* @throws FrameProcessingException If reading shader files fails, or an OpenGL error occurs while
|
* @throws FrameProcessingException If reading shader files fails, or an OpenGL error occurs while
|
||||||
* creating and configuring the OpenGL components.
|
* creating and configuring the OpenGL components.
|
||||||
*/
|
*/
|
||||||
// TODO(b/227625423): Remove @Nullable here and allow the output surface to be @Nullable until
|
|
||||||
// the output surface is requested when the output size becomes available asynchronously
|
|
||||||
// via the final GlTextureProcessor.
|
|
||||||
@Nullable
|
|
||||||
public static FrameProcessorChain create(
|
public static FrameProcessorChain create(
|
||||||
Context context,
|
Context context,
|
||||||
Listener listener,
|
FrameProcessorChain.Listener listener,
|
||||||
float pixelWidthHeightRatio,
|
float pixelWidthHeightRatio,
|
||||||
int inputWidth,
|
int inputWidth,
|
||||||
int inputHeight,
|
int inputHeight,
|
||||||
@ -118,25 +103,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
|
|
||||||
ExecutorService singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME);
|
ExecutorService singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME);
|
||||||
|
|
||||||
Future<Optional<FrameProcessorChain>> frameProcessorChainFuture =
|
Future<FrameProcessorChain> frameProcessorChainFuture =
|
||||||
singleThreadExecutorService.submit(
|
singleThreadExecutorService.submit(
|
||||||
() ->
|
() ->
|
||||||
Optional.fromNullable(
|
createOpenGlObjectsAndFrameProcessorChain(
|
||||||
createOpenGlObjectsAndFrameProcessorChain(
|
context,
|
||||||
context,
|
listener,
|
||||||
listener,
|
pixelWidthHeightRatio,
|
||||||
pixelWidthHeightRatio,
|
inputWidth,
|
||||||
inputWidth,
|
inputHeight,
|
||||||
inputHeight,
|
streamOffsetUs,
|
||||||
streamOffsetUs,
|
effects,
|
||||||
effects,
|
outputSurfaceProvider,
|
||||||
outputSurfaceProvider,
|
debugViewProvider,
|
||||||
debugViewProvider,
|
enableExperimentalHdrEditing,
|
||||||
enableExperimentalHdrEditing,
|
singleThreadExecutorService));
|
||||||
singleThreadExecutorService)));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return frameProcessorChainFuture.get().orNull();
|
return frameProcessorChainFuture.get();
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
throw new FrameProcessingException(e);
|
throw new FrameProcessingException(e);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
@ -146,18 +130,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the OpenGL context, surfaces, textures, and framebuffers, initializes the {@link
|
* Creates the OpenGL context, surfaces, textures, and framebuffers, initializes {@link
|
||||||
* SingleFrameGlTextureProcessor SingleFrameGlTextureProcessors} corresponding to the {@link
|
* GlTextureProcessor} instances corresponding to the {@link GlEffect} instances, and returns a
|
||||||
* GlEffect GlEffects}, and returns a new {@code FrameProcessorChain}.
|
* new {@code FrameProcessorChain}.
|
||||||
*
|
*
|
||||||
* <p>This method must be executed using the {@code singleThreadExecutorService}, as all later
|
* <p>This method must be executed using the {@code singleThreadExecutorService}, as later OpenGL
|
||||||
* OpenGL commands will be called on that thread.
|
* commands will be called on that thread.
|
||||||
*/
|
*/
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@Nullable
|
|
||||||
private static FrameProcessorChain createOpenGlObjectsAndFrameProcessorChain(
|
private static FrameProcessorChain createOpenGlObjectsAndFrameProcessorChain(
|
||||||
Context context,
|
Context context,
|
||||||
Listener listener,
|
FrameProcessorChain.Listener listener,
|
||||||
float pixelWidthHeightRatio,
|
float pixelWidthHeightRatio,
|
||||||
int inputWidth,
|
int inputWidth,
|
||||||
int inputHeight,
|
int inputHeight,
|
||||||
@ -188,132 +171,151 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
|
|
||||||
ImmutableList.Builder<GlMatrixTransformation> matrixTransformationListBuilder =
|
ImmutableList.Builder<GlMatrixTransformation> matrixTransformationListBuilder =
|
||||||
new ImmutableList.Builder<>();
|
new ImmutableList.Builder<>();
|
||||||
// Scale to expand the frame to apply the pixelWidthHeightRatio.
|
if (pixelWidthHeightRatio != 1f) {
|
||||||
if (pixelWidthHeightRatio > 1f) {
|
|
||||||
matrixTransformationListBuilder.add(
|
matrixTransformationListBuilder.add(
|
||||||
new ScaleToFitTransformation.Builder()
|
createPixelWidthHeightRatioTransformation(pixelWidthHeightRatio));
|
||||||
.setScale(/* scaleX= */ pixelWidthHeightRatio, /* scaleY= */ 1f)
|
|
||||||
.build());
|
|
||||||
} else if (pixelWidthHeightRatio < 1f) {
|
|
||||||
matrixTransformationListBuilder.add(
|
|
||||||
new ScaleToFitTransformation.Builder()
|
|
||||||
.setScale(/* scaleX= */ 1f, /* scaleY= */ 1f / pixelWidthHeightRatio)
|
|
||||||
.build());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImmutableList<GlTextureProcessor> textureProcessors =
|
||||||
|
getGlTextureProcessorsForGlEffects(
|
||||||
|
context,
|
||||||
|
effects,
|
||||||
|
eglDisplay,
|
||||||
|
eglContext,
|
||||||
|
matrixTransformationListBuilder,
|
||||||
|
outputSurfaceProvider,
|
||||||
|
streamOffsetUs,
|
||||||
|
listener,
|
||||||
|
debugViewProvider,
|
||||||
|
enableExperimentalHdrEditing);
|
||||||
|
|
||||||
ExternalTextureProcessor externalTextureProcessor =
|
ExternalTextureProcessor externalTextureProcessor =
|
||||||
new ExternalTextureProcessor(context, enableExperimentalHdrEditing);
|
new ExternalTextureProcessor(context, enableExperimentalHdrEditing);
|
||||||
int inputExternalTexId = GlUtil.createExternalTexture();
|
FrameProcessingTaskExecutor frameProcessingTaskExecutor =
|
||||||
Size outputSize = externalTextureProcessor.configure(inputWidth, inputHeight);
|
new FrameProcessingTaskExecutor(singleThreadExecutorService, listener);
|
||||||
ImmutableList.Builder<TextureInfo> intermediateTextures = new ImmutableList.Builder<>();
|
chainTextureProcessorsWithListeners(
|
||||||
ImmutableList.Builder<SingleFrameGlTextureProcessor> textureProcessors =
|
externalTextureProcessor, textureProcessors, frameProcessingTaskExecutor, listener);
|
||||||
new ImmutableList.Builder<SingleFrameGlTextureProcessor>().add(externalTextureProcessor);
|
|
||||||
|
|
||||||
// Combine consecutive GlMatrixTransformations into a single SingleFrameGlTextureProcessor and
|
return new FrameProcessorChain(
|
||||||
// convert all other GlEffects to SingleFrameGlTextureProcessors.
|
eglDisplay,
|
||||||
|
eglContext,
|
||||||
|
frameProcessingTaskExecutor,
|
||||||
|
streamOffsetUs,
|
||||||
|
/* inputExternalTexture= */ new TextureInfo(
|
||||||
|
GlUtil.createExternalTexture(), /* fboId= */ C.INDEX_UNSET, inputWidth, inputHeight),
|
||||||
|
externalTextureProcessor,
|
||||||
|
textureProcessors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new {@link GlMatrixTransformation} to expand or shrink the frame based on the {@code
|
||||||
|
* pixelWidthHeightRatio}.
|
||||||
|
*
|
||||||
|
* <p>If {@code pixelWidthHeightRatio} is 1, this method returns an identity transformation that
|
||||||
|
* can be ignored.
|
||||||
|
*/
|
||||||
|
private static GlMatrixTransformation createPixelWidthHeightRatioTransformation(
|
||||||
|
float pixelWidthHeightRatio) {
|
||||||
|
if (pixelWidthHeightRatio > 1f) {
|
||||||
|
return new ScaleToFitTransformation.Builder()
|
||||||
|
.setScale(/* scaleX= */ pixelWidthHeightRatio, /* scaleY= */ 1f)
|
||||||
|
.build();
|
||||||
|
} else {
|
||||||
|
return new ScaleToFitTransformation.Builder()
|
||||||
|
.setScale(/* scaleX= */ 1f, /* scaleY= */ 1f / pixelWidthHeightRatio)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines consecutive {@link GlMatrixTransformation} instances into a single {@link
|
||||||
|
* MatrixTransformationProcessor} and converts all other {@link GlEffect} instances to separate
|
||||||
|
* {@link GlTextureProcessor} instances.
|
||||||
|
*
|
||||||
|
* <p>The final {@link GlTextureProcessor} is wrapped in a {@link
|
||||||
|
* FinalMatrixTransformationProcessorWrapper} so that it can write directly to the {@linkplain
|
||||||
|
* SurfaceInfo.Provider provided output surface}.
|
||||||
|
*/
|
||||||
|
private static ImmutableList<GlTextureProcessor> getGlTextureProcessorsForGlEffects(
|
||||||
|
Context context,
|
||||||
|
List<GlEffect> effects,
|
||||||
|
EGLDisplay eglDisplay,
|
||||||
|
EGLContext eglContext,
|
||||||
|
ImmutableList.Builder<GlMatrixTransformation> matrixTransformationListBuilder,
|
||||||
|
SurfaceInfo.Provider outputSurfaceProvider,
|
||||||
|
long streamOffsetUs,
|
||||||
|
FrameProcessorChain.Listener listener,
|
||||||
|
Transformer.DebugViewProvider debugViewProvider,
|
||||||
|
boolean enableExperimentalHdrEditing)
|
||||||
|
throws FrameProcessingException {
|
||||||
|
ImmutableList.Builder<GlTextureProcessor> textureProcessorListBuilder =
|
||||||
|
new ImmutableList.Builder<>();
|
||||||
for (int i = 0; i < effects.size(); i++) {
|
for (int i = 0; i < effects.size(); i++) {
|
||||||
GlEffect effect = effects.get(i);
|
GlEffect effect = effects.get(i);
|
||||||
if (effect instanceof GlMatrixTransformation) {
|
if (effect instanceof GlMatrixTransformation) {
|
||||||
matrixTransformationListBuilder.add((GlMatrixTransformation) effect);
|
matrixTransformationListBuilder.add((GlMatrixTransformation) effect);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImmutableList<GlMatrixTransformation> matrixTransformations =
|
ImmutableList<GlMatrixTransformation> matrixTransformations =
|
||||||
matrixTransformationListBuilder.build();
|
matrixTransformationListBuilder.build();
|
||||||
if (!matrixTransformations.isEmpty()) {
|
if (!matrixTransformations.isEmpty()) {
|
||||||
MatrixTransformationProcessor matrixTransformationProcessor =
|
textureProcessorListBuilder.add(
|
||||||
new MatrixTransformationProcessor(context, matrixTransformations);
|
new MatrixTransformationProcessor(context, matrixTransformations));
|
||||||
intermediateTextures.add(createTexture(outputSize.getWidth(), outputSize.getHeight()));
|
|
||||||
outputSize =
|
|
||||||
matrixTransformationProcessor.configure(outputSize.getWidth(), outputSize.getHeight());
|
|
||||||
textureProcessors.add(matrixTransformationProcessor);
|
|
||||||
matrixTransformationListBuilder = new ImmutableList.Builder<>();
|
matrixTransformationListBuilder = new ImmutableList.Builder<>();
|
||||||
}
|
}
|
||||||
intermediateTextures.add(createTexture(outputSize.getWidth(), outputSize.getHeight()));
|
textureProcessorListBuilder.add(effect.toGlTextureProcessor(context));
|
||||||
SingleFrameGlTextureProcessor textureProcessor = effect.toGlTextureProcessor(context);
|
|
||||||
outputSize = textureProcessor.configure(outputSize.getWidth(), outputSize.getHeight());
|
|
||||||
textureProcessors.add(textureProcessor);
|
|
||||||
}
|
}
|
||||||
|
textureProcessorListBuilder.add(
|
||||||
// TODO(b/227625423): Request the output surface during processing when the output size becomes
|
new FinalMatrixTransformationProcessorWrapper(
|
||||||
// available asynchronously via the final GlTextureProcessor instead of requesting it here.
|
context,
|
||||||
// This will also avoid needing to return null here when no surface is provided.
|
eglDisplay,
|
||||||
Size requestedOutputSize =
|
eglContext,
|
||||||
MatrixUtils.configureAndGetOutputSize(
|
matrixTransformationListBuilder.build(),
|
||||||
outputSize.getWidth(), outputSize.getHeight(), matrixTransformationListBuilder.build());
|
outputSurfaceProvider,
|
||||||
@Nullable
|
streamOffsetUs,
|
||||||
SurfaceInfo outputSurfaceInfo =
|
listener,
|
||||||
outputSurfaceProvider.getSurfaceInfo(
|
debugViewProvider,
|
||||||
requestedOutputSize.getWidth(), requestedOutputSize.getHeight());
|
enableExperimentalHdrEditing));
|
||||||
if (outputSurfaceInfo == null) {
|
return textureProcessorListBuilder.build();
|
||||||
Log.d(TAG, "No output surface provided.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outputSurfaceInfo.orientationDegrees != 0) {
|
|
||||||
matrixTransformationListBuilder.add(
|
|
||||||
new ScaleToFitTransformation.Builder()
|
|
||||||
.setRotationDegrees(outputSurfaceInfo.orientationDegrees)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
if (outputSurfaceInfo.width != outputSize.getWidth()
|
|
||||||
|| outputSurfaceInfo.height != outputSize.getHeight()) {
|
|
||||||
matrixTransformationListBuilder.add(
|
|
||||||
Presentation.createForWidthAndHeight(
|
|
||||||
outputSurfaceInfo.width, outputSurfaceInfo.height, Presentation.LAYOUT_SCALE_TO_FIT));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert final list of matrix transformations (including additional transformations for the
|
|
||||||
// output surface) to a SingleFrameGlTextureProcessors.
|
|
||||||
ImmutableList<GlMatrixTransformation> matrixTransformations =
|
|
||||||
matrixTransformationListBuilder.build();
|
|
||||||
if (!matrixTransformations.isEmpty()) {
|
|
||||||
intermediateTextures.add(createTexture(outputSize.getWidth(), outputSize.getHeight()));
|
|
||||||
MatrixTransformationProcessor matrixTransformationProcessor =
|
|
||||||
new MatrixTransformationProcessor(context, matrixTransformations);
|
|
||||||
outputSize =
|
|
||||||
matrixTransformationProcessor.configure(outputSize.getWidth(), outputSize.getHeight());
|
|
||||||
checkState(outputSize.getWidth() == outputSurfaceInfo.width);
|
|
||||||
checkState(outputSize.getHeight() == outputSurfaceInfo.height);
|
|
||||||
textureProcessors.add(matrixTransformationProcessor);
|
|
||||||
}
|
|
||||||
|
|
||||||
EGLSurface outputEglSurface;
|
|
||||||
if (enableExperimentalHdrEditing) {
|
|
||||||
// TODO(b/227624622): Don't assume BT.2020 PQ input/output.
|
|
||||||
outputEglSurface = GlUtil.getEglSurfaceBt2020Pq(eglDisplay, outputSurfaceInfo.surface);
|
|
||||||
} else {
|
|
||||||
outputEglSurface = GlUtil.getEglSurface(eglDisplay, outputSurfaceInfo.surface);
|
|
||||||
}
|
|
||||||
return new FrameProcessorChain(
|
|
||||||
eglDisplay,
|
|
||||||
eglContext,
|
|
||||||
singleThreadExecutorService,
|
|
||||||
inputExternalTexId,
|
|
||||||
streamOffsetUs,
|
|
||||||
intermediateTextures.build(),
|
|
||||||
textureProcessors.build(),
|
|
||||||
outputSurfaceInfo.width,
|
|
||||||
outputSurfaceInfo.height,
|
|
||||||
outputEglSurface,
|
|
||||||
listener,
|
|
||||||
debugViewProvider.getDebugPreviewSurfaceView(
|
|
||||||
outputSurfaceInfo.width, outputSurfaceInfo.height),
|
|
||||||
enableExperimentalHdrEditing);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TextureInfo createTexture(int outputWidth, int outputHeight)
|
/**
|
||||||
throws GlUtil.GlException {
|
* Chains the given {@link GlTextureProcessor} instances using {@link
|
||||||
int texId = GlUtil.createTexture(outputWidth, outputHeight);
|
* ChainingGlTextureProcessorListener} instances.
|
||||||
int fboId = GlUtil.createFboForTexture(texId);
|
*
|
||||||
return new TextureInfo(texId, fboId, outputWidth, outputHeight);
|
* <p>The {@link ExternalTextureProcessor} is the first processor in the chain.
|
||||||
|
*/
|
||||||
|
private static void chainTextureProcessorsWithListeners(
|
||||||
|
ExternalTextureProcessor externalTextureProcessor,
|
||||||
|
ImmutableList<GlTextureProcessor> textureProcessors,
|
||||||
|
FrameProcessingTaskExecutor frameProcessingTaskExecutor,
|
||||||
|
FrameProcessorChain.Listener listener) {
|
||||||
|
externalTextureProcessor.setListener(
|
||||||
|
new ChainingGlTextureProcessorListener(
|
||||||
|
/* previousGlTextureProcessor= */ null,
|
||||||
|
textureProcessors.get(0),
|
||||||
|
frameProcessingTaskExecutor,
|
||||||
|
listener));
|
||||||
|
GlTextureProcessor previousGlTextureProcessor = externalTextureProcessor;
|
||||||
|
for (int i = 0; i < textureProcessors.size(); i++) {
|
||||||
|
GlTextureProcessor glTextureProcessor = textureProcessors.get(i);
|
||||||
|
@Nullable
|
||||||
|
GlTextureProcessor nextGlTextureProcessor =
|
||||||
|
i + 1 < textureProcessors.size() ? textureProcessors.get(i + 1) : null;
|
||||||
|
glTextureProcessor.setListener(
|
||||||
|
new ChainingGlTextureProcessorListener(
|
||||||
|
previousGlTextureProcessor,
|
||||||
|
nextGlTextureProcessor,
|
||||||
|
frameProcessingTaskExecutor,
|
||||||
|
listener));
|
||||||
|
previousGlTextureProcessor = glTextureProcessor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String TAG = "FrameProcessorChain";
|
private static final String TAG = "FrameProcessorChain";
|
||||||
private static final String THREAD_NAME = "Transformer:FrameProcessorChain";
|
private static final String THREAD_NAME = "Transformer:FrameProcessorChain";
|
||||||
private static final long RELEASE_WAIT_TIME_MS = 100;
|
private static final long RELEASE_WAIT_TIME_MS = 100;
|
||||||
|
|
||||||
private final boolean enableExperimentalHdrEditing;
|
|
||||||
private final EGLDisplay eglDisplay;
|
private final EGLDisplay eglDisplay;
|
||||||
private final EGLContext eglContext;
|
private final EGLContext eglContext;
|
||||||
private final FrameProcessingTaskExecutor frameProcessingTaskExecutor;
|
private final FrameProcessingTaskExecutor frameProcessingTaskExecutor;
|
||||||
@ -322,100 +324,52 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
* timestamps, in microseconds.
|
* timestamps, in microseconds.
|
||||||
*/
|
*/
|
||||||
private final long streamOffsetUs;
|
private final long streamOffsetUs;
|
||||||
/** Number of frames {@linkplain #registerInputFrame() registered} but not fully processed. */
|
/**
|
||||||
private final AtomicInteger pendingFrameCount;
|
* Number of frames {@linkplain #registerInputFrame() registered} but not processed off the {@link
|
||||||
/** Wraps the {@link #inputSurfaceTexture}. */
|
* #inputSurfaceTexture} yet.
|
||||||
private final Surface inputSurface;
|
*/
|
||||||
|
private final AtomicInteger pendingInputFrameCount;
|
||||||
/** Associated with an OpenGL external texture. */
|
/** Associated with an OpenGL external texture. */
|
||||||
private final SurfaceTexture inputSurfaceTexture;
|
private final SurfaceTexture inputSurfaceTexture;
|
||||||
/** Identifier of the OpenGL texture associated with the input {@link SurfaceTexture}. */
|
/** Wraps the {@link #inputSurfaceTexture}. */
|
||||||
private final int inputExternalTexId;
|
private final Surface inputSurface;
|
||||||
/** Transformation matrix associated with the {@link #inputSurfaceTexture}. */
|
|
||||||
private final float[] textureTransformMatrix;
|
|
||||||
|
|
||||||
/**
|
private final float[] inputSurfaceTextureTransformMatrix;
|
||||||
* Contains an {@link ExternalTextureProcessor} at the 0th index and optionally other {@link
|
private final TextureInfo inputExternalTexture;
|
||||||
* SingleFrameGlTextureProcessor SingleFrameGlTextureProcessors} at indices >= 1.
|
private final ExternalTextureProcessor inputExternalTextureProcessor;
|
||||||
*/
|
private final ImmutableList<GlTextureProcessor> textureProcessors;
|
||||||
private final ImmutableList<SingleFrameGlTextureProcessor> textureProcessors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link TextureInfo} instances describing the intermediate textures that receive output from the
|
|
||||||
* previous {@link SingleFrameGlTextureProcessor}, and provide input for the following {@link
|
|
||||||
* SingleFrameGlTextureProcessor}.
|
|
||||||
*/
|
|
||||||
private final ImmutableList<TextureInfo> intermediateTextures;
|
|
||||||
|
|
||||||
private final Listener listener;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prevents further frame processing tasks from being scheduled or executed after {@link
|
|
||||||
* #release()} is called or an exception occurred.
|
|
||||||
*/
|
|
||||||
private final AtomicBoolean stopProcessing;
|
|
||||||
|
|
||||||
private final int outputWidth;
|
|
||||||
private final int outputHeight;
|
|
||||||
/**
|
|
||||||
* Wraps the output {@link Surface} that is populated with the output of the final {@link
|
|
||||||
* SingleFrameGlTextureProcessor} for each frame.
|
|
||||||
*/
|
|
||||||
private final EGLSurface outputEglSurface;
|
|
||||||
/**
|
|
||||||
* Wraps a debug {@link SurfaceView} that is populated with the output of the final {@link
|
|
||||||
* SingleFrameGlTextureProcessor} for each frame.
|
|
||||||
*/
|
|
||||||
private @MonotonicNonNull SurfaceViewWrapper debugSurfaceViewWrapper;
|
|
||||||
|
|
||||||
private boolean inputStreamEnded;
|
private boolean inputStreamEnded;
|
||||||
|
|
||||||
// TODO(b/227625423): accept GlTextureProcessors instead of SingleFrameGlTextureProcessors once
|
|
||||||
// this interface exists.
|
|
||||||
private FrameProcessorChain(
|
private FrameProcessorChain(
|
||||||
EGLDisplay eglDisplay,
|
EGLDisplay eglDisplay,
|
||||||
EGLContext eglContext,
|
EGLContext eglContext,
|
||||||
ExecutorService singleThreadExecutorService,
|
FrameProcessingTaskExecutor frameProcessingTaskExecutor,
|
||||||
int inputExternalTexId,
|
|
||||||
long streamOffsetUs,
|
long streamOffsetUs,
|
||||||
ImmutableList<TextureInfo> intermediateTextures,
|
TextureInfo inputExternalTexture,
|
||||||
ImmutableList<SingleFrameGlTextureProcessor> textureProcessors,
|
ExternalTextureProcessor inputExternalTextureProcessor,
|
||||||
int outputWidth,
|
ImmutableList<GlTextureProcessor> textureProcessors) {
|
||||||
int outputHeight,
|
|
||||||
EGLSurface outputEglSurface,
|
|
||||||
Listener listener,
|
|
||||||
@Nullable SurfaceView debugSurfaceView,
|
|
||||||
boolean enableExperimentalHdrEditing) {
|
|
||||||
checkState(!textureProcessors.isEmpty());
|
checkState(!textureProcessors.isEmpty());
|
||||||
|
|
||||||
this.eglDisplay = eglDisplay;
|
this.eglDisplay = eglDisplay;
|
||||||
this.eglContext = eglContext;
|
this.eglContext = eglContext;
|
||||||
this.inputExternalTexId = inputExternalTexId;
|
this.frameProcessingTaskExecutor = frameProcessingTaskExecutor;
|
||||||
this.streamOffsetUs = streamOffsetUs;
|
this.streamOffsetUs = streamOffsetUs;
|
||||||
this.intermediateTextures = intermediateTextures;
|
this.inputExternalTexture = inputExternalTexture;
|
||||||
|
this.inputExternalTextureProcessor = inputExternalTextureProcessor;
|
||||||
this.textureProcessors = textureProcessors;
|
this.textureProcessors = textureProcessors;
|
||||||
this.outputWidth = outputWidth;
|
|
||||||
this.outputHeight = outputHeight;
|
|
||||||
this.outputEglSurface = outputEglSurface;
|
|
||||||
this.listener = listener;
|
|
||||||
this.stopProcessing = new AtomicBoolean();
|
|
||||||
this.enableExperimentalHdrEditing = enableExperimentalHdrEditing;
|
|
||||||
|
|
||||||
frameProcessingTaskExecutor =
|
pendingInputFrameCount = new AtomicInteger();
|
||||||
new FrameProcessingTaskExecutor(singleThreadExecutorService, listener);
|
inputSurfaceTexture = new SurfaceTexture(inputExternalTexture.texId);
|
||||||
pendingFrameCount = new AtomicInteger();
|
|
||||||
inputSurfaceTexture = new SurfaceTexture(inputExternalTexId);
|
|
||||||
inputSurface = new Surface(inputSurfaceTexture);
|
inputSurface = new Surface(inputSurfaceTexture);
|
||||||
textureTransformMatrix = new float[16];
|
inputSurfaceTextureTransformMatrix = new float[16];
|
||||||
if (debugSurfaceView != null) {
|
|
||||||
debugSurfaceViewWrapper = new SurfaceViewWrapper(debugSurfaceView);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the input {@link Surface}. */
|
/** Returns the input {@link Surface}. */
|
||||||
public Surface getInputSurface() {
|
public Surface getInputSurface() {
|
||||||
// TODO(b/227625423): Allow input surface to be recreated for input size change.
|
// TODO(b/227625423): Allow input surface to be recreated for input size change.
|
||||||
inputSurfaceTexture.setOnFrameAvailableListener(
|
inputSurfaceTexture.setOnFrameAvailableListener(
|
||||||
surfaceTexture -> frameProcessingTaskExecutor.submit(this::processFrame));
|
surfaceTexture -> frameProcessingTaskExecutor.submit(this::processInputFrame));
|
||||||
return inputSurface;
|
return inputSurface;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,15 +382,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
*/
|
*/
|
||||||
public void registerInputFrame() {
|
public void registerInputFrame() {
|
||||||
checkState(!inputStreamEnded);
|
checkState(!inputStreamEnded);
|
||||||
pendingFrameCount.incrementAndGet();
|
pendingInputFrameCount.incrementAndGet();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the number of input frames that have been {@linkplain #registerInputFrame() registered}
|
* Returns the number of input frames that have been {@linkplain #registerInputFrame() registered}
|
||||||
* but not completely processed yet.
|
* but not processed off the {@linkplain #getInputSurface() input surface} yet.
|
||||||
*/
|
*/
|
||||||
public int getPendingFrameCount() {
|
public int getPendingInputFrameCount() {
|
||||||
return pendingFrameCount.get();
|
return pendingInputFrameCount.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -447,7 +401,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
public void signalEndOfInputStream() {
|
public void signalEndOfInputStream() {
|
||||||
checkState(!inputStreamEnded);
|
checkState(!inputStreamEnded);
|
||||||
inputStreamEnded = true;
|
inputStreamEnded = true;
|
||||||
frameProcessingTaskExecutor.submit(this::signalEndOfOutputStream);
|
frameProcessingTaskExecutor.submit(this::processEndOfInputStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -461,7 +415,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
* <p>This method blocks until all OpenGL resources are released or releasing times out.
|
* <p>This method blocks until all OpenGL resources are released or releasing times out.
|
||||||
*/
|
*/
|
||||||
public void release() {
|
public void release() {
|
||||||
stopProcessing.set(true);
|
|
||||||
try {
|
try {
|
||||||
frameProcessingTaskExecutor.release(
|
frameProcessingTaskExecutor.release(
|
||||||
/* releaseTask= */ this::releaseTextureProcessorsAndDestroyGlContext,
|
/* releaseTask= */ this::releaseTextureProcessorsAndDestroyGlContext,
|
||||||
@ -475,163 +428,61 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes an input frame.
|
* Processes an input frame from the {@linkplain #getInputSurface() external input surface
|
||||||
|
* texture}.
|
||||||
*
|
*
|
||||||
* <p>This method must be called on the {@linkplain #THREAD_NAME background thread}.
|
* <p>This method must be called on the {@linkplain #THREAD_NAME background thread}.
|
||||||
*/
|
*/
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private void processFrame() throws FrameProcessingException {
|
private void processInputFrame() {
|
||||||
checkState(Thread.currentThread().getName().equals(THREAD_NAME));
|
checkState(Thread.currentThread().getName().equals(THREAD_NAME));
|
||||||
|
if (!inputExternalTextureProcessor.acceptsInputFrame()) {
|
||||||
|
frameProcessingTaskExecutor.submit(this::processInputFrame); // Try again later.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
inputSurfaceTexture.updateTexImage();
|
inputSurfaceTexture.updateTexImage();
|
||||||
long inputFrameTimeNs = inputSurfaceTexture.getTimestamp();
|
long inputFrameTimeNs = inputSurfaceTexture.getTimestamp();
|
||||||
// Correct for the stream offset so processors see original media presentation timestamps.
|
// Correct for the stream offset so processors see original media presentation timestamps.
|
||||||
long presentationTimeUs = inputFrameTimeNs / 1000 - streamOffsetUs;
|
long presentationTimeUs = inputFrameTimeNs / 1000 - streamOffsetUs;
|
||||||
inputSurfaceTexture.getTransformMatrix(textureTransformMatrix);
|
inputSurfaceTexture.getTransformMatrix(inputSurfaceTextureTransformMatrix);
|
||||||
((ExternalTextureProcessor) textureProcessors.get(0))
|
inputExternalTextureProcessor.setTextureTransformMatrix(inputSurfaceTextureTransformMatrix);
|
||||||
.setTextureTransformMatrix(textureTransformMatrix);
|
checkState(
|
||||||
int inputTexId = inputExternalTexId;
|
inputExternalTextureProcessor.maybeQueueInputFrame(
|
||||||
|
inputExternalTexture, presentationTimeUs));
|
||||||
try {
|
checkState(pendingInputFrameCount.getAndDecrement() > 0);
|
||||||
for (int i = 0; i < textureProcessors.size() - 1; i++) {
|
// After the inputExternalTextureProcessor has produced an output frame, it is processed
|
||||||
if (stopProcessing.get()) {
|
// asynchronously by the texture processors chained after it.
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TextureInfo outputTexture = intermediateTextures.get(i);
|
|
||||||
GlUtil.focusFramebuffer(
|
|
||||||
eglDisplay,
|
|
||||||
eglContext,
|
|
||||||
outputEglSurface,
|
|
||||||
outputTexture.fboId,
|
|
||||||
outputTexture.width,
|
|
||||||
outputTexture.height);
|
|
||||||
GlUtil.clearOutputFrame();
|
|
||||||
textureProcessors.get(i).drawFrame(inputTexId, presentationTimeUs);
|
|
||||||
inputTexId = outputTexture.texId;
|
|
||||||
}
|
|
||||||
GlUtil.focusEglSurface(eglDisplay, eglContext, outputEglSurface, outputWidth, outputHeight);
|
|
||||||
GlUtil.clearOutputFrame();
|
|
||||||
getLast(textureProcessors).drawFrame(inputTexId, presentationTimeUs);
|
|
||||||
|
|
||||||
EGLExt.eglPresentationTimeANDROID(eglDisplay, outputEglSurface, inputFrameTimeNs);
|
|
||||||
EGL14.eglSwapBuffers(eglDisplay, outputEglSurface);
|
|
||||||
} catch (GlUtil.GlException e) {
|
|
||||||
throw new FrameProcessingException(e, presentationTimeUs);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (debugSurfaceViewWrapper != null) {
|
|
||||||
long finalPresentationTimeUs = presentationTimeUs;
|
|
||||||
int finalInputTexId = inputTexId;
|
|
||||||
debugSurfaceViewWrapper.maybeRenderToSurfaceView(
|
|
||||||
() -> {
|
|
||||||
GlUtil.clearOutputFrame();
|
|
||||||
getLast(textureProcessors).drawFrame(finalInputTexId, finalPresentationTimeUs);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (FrameProcessingException | GlUtil.GlException e) {
|
|
||||||
Log.d(TAG, "Error rendering to debug preview", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
checkState(pendingFrameCount.getAndDecrement() > 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Calls {@link Listener#onFrameProcessingEnded()} once no more frames are pending. */
|
/**
|
||||||
|
* Propagates the end-of-stream signal through the texture processors once no more input frames
|
||||||
|
* are pending.
|
||||||
|
*
|
||||||
|
* <p>This method must be called on the {@linkplain #THREAD_NAME background thread}.
|
||||||
|
*/
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private void signalEndOfOutputStream() {
|
private void processEndOfInputStream() {
|
||||||
if (getPendingFrameCount() == 0) {
|
if (getPendingInputFrameCount() == 0) {
|
||||||
listener.onFrameProcessingEnded();
|
// Propagates the end of stream signal through the chained texture processors.
|
||||||
|
inputExternalTextureProcessor.signalEndOfInputStream();
|
||||||
} else {
|
} else {
|
||||||
frameProcessingTaskExecutor.submit(this::signalEndOfOutputStream);
|
frameProcessingTaskExecutor.submit(this::processEndOfInputStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Releases the {@link SingleFrameGlTextureProcessor SingleFrameGlTextureProcessors} and destroys
|
* Releases the {@link GlTextureProcessor} instances and destroys the OpenGL context.
|
||||||
* the OpenGL context.
|
|
||||||
*
|
*
|
||||||
* <p>This method must be called on the {@linkplain #THREAD_NAME background thread}.
|
* <p>This method must be called on the {@linkplain #THREAD_NAME background thread}.
|
||||||
*/
|
*/
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private void releaseTextureProcessorsAndDestroyGlContext()
|
private void releaseTextureProcessorsAndDestroyGlContext()
|
||||||
throws GlUtil.GlException, FrameProcessingException {
|
throws GlUtil.GlException, FrameProcessingException {
|
||||||
|
inputExternalTextureProcessor.release();
|
||||||
for (int i = 0; i < textureProcessors.size(); i++) {
|
for (int i = 0; i < textureProcessors.size(); i++) {
|
||||||
textureProcessors.get(i).release();
|
textureProcessors.get(i).release();
|
||||||
}
|
}
|
||||||
GlUtil.destroyEglContext(eglDisplay, eglContext);
|
GlUtil.destroyEglContext(eglDisplay, eglContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper around a {@link SurfaceView} that keeps track of whether the output surface is valid,
|
|
||||||
* and makes rendering a no-op if not.
|
|
||||||
*/
|
|
||||||
private final class SurfaceViewWrapper implements SurfaceHolder.Callback {
|
|
||||||
|
|
||||||
@GuardedBy("this")
|
|
||||||
@Nullable
|
|
||||||
private Surface surface;
|
|
||||||
|
|
||||||
@GuardedBy("this")
|
|
||||||
@Nullable
|
|
||||||
private EGLSurface eglSurface;
|
|
||||||
|
|
||||||
private int width;
|
|
||||||
private int height;
|
|
||||||
|
|
||||||
public SurfaceViewWrapper(SurfaceView surfaceView) {
|
|
||||||
surfaceView.getHolder().addCallback(this);
|
|
||||||
surface = surfaceView.getHolder().getSurface();
|
|
||||||
width = surfaceView.getWidth();
|
|
||||||
height = surfaceView.getHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Focuses the wrapped surface view's surface as an {@link EGLSurface}, renders using {@code
|
|
||||||
* renderingTask} and swaps buffers, if the view's holder has a valid surface. Does nothing
|
|
||||||
* otherwise.
|
|
||||||
*/
|
|
||||||
@WorkerThread
|
|
||||||
public synchronized void maybeRenderToSurfaceView(FrameProcessingTask renderingTask)
|
|
||||||
throws GlUtil.GlException, FrameProcessingException {
|
|
||||||
if (surface == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eglSurface == null) {
|
|
||||||
if (enableExperimentalHdrEditing) {
|
|
||||||
eglSurface = GlUtil.getEglSurfaceBt2020Pq(eglDisplay, surface);
|
|
||||||
} else {
|
|
||||||
eglSurface = GlUtil.getEglSurface(eglDisplay, surface);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EGLSurface eglSurface = this.eglSurface;
|
|
||||||
GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, width, height);
|
|
||||||
renderingTask.run();
|
|
||||||
EGL14.eglSwapBuffers(eglDisplay, eglSurface);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void surfaceCreated(SurfaceHolder holder) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void surfaceChanged(
|
|
||||||
SurfaceHolder holder, int format, int width, int height) {
|
|
||||||
this.width = width;
|
|
||||||
this.height = height;
|
|
||||||
Surface newSurface = holder.getSurface();
|
|
||||||
if (surface == null || !surface.equals(newSurface)) {
|
|
||||||
surface = newSurface;
|
|
||||||
eglSurface = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void surfaceDestroyed(SurfaceHolder holder) {
|
|
||||||
surface = null;
|
|
||||||
eglSurface = null;
|
|
||||||
width = C.LENGTH_UNSET;
|
|
||||||
height = C.LENGTH_UNSET;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,16 @@ public abstract class SingleFrameGlTextureProcessor implements GlTextureProcesso
|
|||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the {@code SingleFrameGlTextureProcessor} can accept an input frame.
|
||||||
|
*
|
||||||
|
* <p>If this method returns {@code true}, the next call to {@link #maybeQueueInputFrame(
|
||||||
|
* TextureInfo, long)} will also return {@code true}.
|
||||||
|
*/
|
||||||
|
public boolean acceptsInputFrame() {
|
||||||
|
return !outputTextureInUse;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final boolean maybeQueueInputFrame(TextureInfo inputTexture, long presentationTimeUs) {
|
public final boolean maybeQueueInputFrame(TextureInfo inputTexture, long presentationTimeUs) {
|
||||||
if (outputTextureInUse) {
|
if (outputTextureInUse) {
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
package androidx.media3.transformer;
|
package androidx.media3.transformer;
|
||||||
|
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
@ -32,7 +31,6 @@ import com.google.common.collect.ImmutableList;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
import org.checkerframework.dataflow.qual.Pure;
|
import org.checkerframework.dataflow.qual.Pure;
|
||||||
|
|
||||||
@ -91,8 +89,6 @@ import org.checkerframework.dataflow.qual.Pure;
|
|||||||
effectsListBuilder.add(Presentation.createForHeight(transformationRequest.outputHeight));
|
effectsListBuilder.add(Presentation.createForHeight(transformationRequest.outputHeight));
|
||||||
}
|
}
|
||||||
|
|
||||||
AtomicReference<TransformationException> encoderInitializationException =
|
|
||||||
new AtomicReference<>();
|
|
||||||
encoderWrapper =
|
encoderWrapper =
|
||||||
new EncoderWrapper(
|
new EncoderWrapper(
|
||||||
encoderFactory,
|
encoderFactory,
|
||||||
@ -100,9 +96,8 @@ import org.checkerframework.dataflow.qual.Pure;
|
|||||||
allowedOutputMimeTypes,
|
allowedOutputMimeTypes,
|
||||||
transformationRequest,
|
transformationRequest,
|
||||||
fallbackListener,
|
fallbackListener,
|
||||||
encoderInitializationException);
|
asyncErrorListener);
|
||||||
|
|
||||||
@Nullable FrameProcessorChain frameProcessorChain;
|
|
||||||
try {
|
try {
|
||||||
frameProcessorChain =
|
frameProcessorChain =
|
||||||
FrameProcessorChain.create(
|
FrameProcessorChain.create(
|
||||||
@ -137,12 +132,6 @@ import org.checkerframework.dataflow.qual.Pure;
|
|||||||
e, TransformationException.ERROR_CODE_GL_INIT_FAILED);
|
e, TransformationException.ERROR_CODE_GL_INIT_FAILED);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frameProcessorChain == null) {
|
|
||||||
// Failed to create FrameProcessorChain because the encoder could not provide a surface.
|
|
||||||
throw checkStateNotNull(encoderInitializationException.get());
|
|
||||||
}
|
|
||||||
this.frameProcessorChain = frameProcessorChain;
|
|
||||||
|
|
||||||
decoder =
|
decoder =
|
||||||
decoderFactory.createForVideoDecoding(
|
decoderFactory.createForVideoDecoding(
|
||||||
inputFormat,
|
inputFormat,
|
||||||
@ -266,7 +255,7 @@ import org.checkerframework.dataflow.qual.Pure;
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (maxPendingFrameCount != Codec.UNLIMITED_PENDING_FRAME_COUNT
|
if (maxPendingFrameCount != Codec.UNLIMITED_PENDING_FRAME_COUNT
|
||||||
&& frameProcessorChain.getPendingFrameCount() == maxPendingFrameCount) {
|
&& frameProcessorChain.getPendingInputFrameCount() == maxPendingFrameCount) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,7 +292,7 @@ import org.checkerframework.dataflow.qual.Pure;
|
|||||||
private final List<String> allowedOutputMimeTypes;
|
private final List<String> allowedOutputMimeTypes;
|
||||||
private final TransformationRequest transformationRequest;
|
private final TransformationRequest transformationRequest;
|
||||||
private final FallbackListener fallbackListener;
|
private final FallbackListener fallbackListener;
|
||||||
private final AtomicReference<TransformationException> encoderInitializationException;
|
private final Transformer.AsyncErrorListener asyncErrorListener;
|
||||||
|
|
||||||
private @MonotonicNonNull SurfaceInfo encoderSurfaceInfo;
|
private @MonotonicNonNull SurfaceInfo encoderSurfaceInfo;
|
||||||
|
|
||||||
@ -317,14 +306,14 @@ import org.checkerframework.dataflow.qual.Pure;
|
|||||||
List<String> allowedOutputMimeTypes,
|
List<String> allowedOutputMimeTypes,
|
||||||
TransformationRequest transformationRequest,
|
TransformationRequest transformationRequest,
|
||||||
FallbackListener fallbackListener,
|
FallbackListener fallbackListener,
|
||||||
AtomicReference<TransformationException> encoderInitializationException) {
|
Transformer.AsyncErrorListener asyncErrorListener) {
|
||||||
|
|
||||||
this.encoderFactory = encoderFactory;
|
this.encoderFactory = encoderFactory;
|
||||||
this.inputFormat = inputFormat;
|
this.inputFormat = inputFormat;
|
||||||
this.allowedOutputMimeTypes = allowedOutputMimeTypes;
|
this.allowedOutputMimeTypes = allowedOutputMimeTypes;
|
||||||
this.transformationRequest = transformationRequest;
|
this.transformationRequest = transformationRequest;
|
||||||
this.fallbackListener = fallbackListener;
|
this.fallbackListener = fallbackListener;
|
||||||
this.encoderInitializationException = encoderInitializationException;
|
this.asyncErrorListener = asyncErrorListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -365,7 +354,7 @@ import org.checkerframework.dataflow.qual.Pure;
|
|||||||
encoder =
|
encoder =
|
||||||
encoderFactory.createForVideoEncoding(requestedEncoderFormat, allowedOutputMimeTypes);
|
encoderFactory.createForVideoEncoding(requestedEncoderFormat, allowedOutputMimeTypes);
|
||||||
} catch (TransformationException e) {
|
} catch (TransformationException e) {
|
||||||
encoderInitializationException.set(e);
|
asyncErrorListener.onTransformationException(e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Format encoderSupportedFormat = encoder.getConfigurationFormat();
|
Format encoderSupportedFormat = encoder.getConfigurationFormat();
|
||||||
|
@ -29,7 +29,6 @@ import androidx.media3.common.util.ListenerSet;
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
@ -52,7 +51,7 @@ public final class VideoEncoderWrapperTest {
|
|||||||
/* allowedOutputMimeTypes= */ ImmutableList.of(),
|
/* allowedOutputMimeTypes= */ ImmutableList.of(),
|
||||||
emptyTransformationRequest,
|
emptyTransformationRequest,
|
||||||
fallbackListener,
|
fallbackListener,
|
||||||
new AtomicReference<>());
|
mock(Transformer.AsyncErrorListener.class));
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void registerTrack() {
|
public void registerTrack() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user