From faa0644ed1736395acfc8f47f3753af6eedcab9d Mon Sep 17 00:00:00 2001 From: Googler Date: Tue, 6 May 2025 08:49:34 -0700 Subject: [PATCH] Add an AndroidTest for FinalShaderProgramWrapper PiperOrigin-RevId: 755389859 --- .../effect/FinalShaderProgramWrapperTest.java | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 libraries/effect/src/androidTest/java/androidx/media3/effect/FinalShaderProgramWrapperTest.java diff --git a/libraries/effect/src/androidTest/java/androidx/media3/effect/FinalShaderProgramWrapperTest.java b/libraries/effect/src/androidTest/java/androidx/media3/effect/FinalShaderProgramWrapperTest.java new file mode 100644 index 0000000000..b31f648566 --- /dev/null +++ b/libraries/effect/src/androidTest/java/androidx/media3/effect/FinalShaderProgramWrapperTest.java @@ -0,0 +1,313 @@ +/* + * 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.GlUtil.createTexture; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.PixelFormat; +import android.media.ImageReader; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLSurface; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.ColorInfo; +import androidx.media3.common.GlObjectsProvider; +import androidx.media3.common.GlTextureInfo; +import androidx.media3.common.SurfaceInfo; +import androidx.media3.common.VideoFrameProcessingException; +import androidx.media3.common.VideoFrameProcessor; +import androidx.media3.common.VideoFrameProcessor.Listener; +import androidx.media3.common.util.GlUtil; +import androidx.media3.effect.GlShaderProgram.InputListener; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.ArrayList; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for {@link FinalShaderProgramWrapper}. + * + *

The thread the test runs on is the VideoFrameProcessing and Gl thread, to make assertions + * easier and avoid race conditions. + */ +@RunWith(AndroidJUnit4.class) +public final class FinalShaderProgramWrapperTest { + private @MonotonicNonNull FinalShaderProgramWrapper finalShaderProgramWrapper; + + private @MonotonicNonNull EGLDisplay eglDisplay; + private @MonotonicNonNull EGLContext eglContext; + private @MonotonicNonNull GlObjectsProvider glObjectsProvider; + private @MonotonicNonNull EGLSurface placeholderEglSurface; + private @MonotonicNonNull VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor; + @Nullable private VideoFrameProcessingException videoFrameProcessingException; + @Nullable private VideoFrameProcessor.Listener videoFrameProcessorListener; + @Nullable private InputListener inputListener; + @Nullable private FinalShaderProgramWrapper.Listener listener; + private @MonotonicNonNull SurfaceInfo outputSurfaceInfo; + private @MonotonicNonNull List inputTextureInfos; + + /** + * All presentationTimeUs of frames sent to {@link + * VideoFrameProcessor.Listener#onOutputFrameAvailableForRendering} + */ + private @MonotonicNonNull List presentationTimesUsAvailableForRendering; + + /** + * All {@link GlTextureInfo} sent to {@link GlShaderProgram.InputListener#onInputFrameProcessed}. + */ + private @MonotonicNonNull List processedTextures; + + /** + * All presentationTimeUs of frames sent to {@link + * FinalShaderProgramWrapper.Listener#onFrameRendered}. + */ + private @MonotonicNonNull List renderedPresentationTimesUs; + + /** + * A list of the last presentationTimeUs sent to {@link + * FinalShaderProgramWrapper.Listener#onFrameRendered} when {@link + * FinalShaderProgramWrapper.Listener#onInputStreamProcessed} is called. + */ + private @MonotonicNonNull List endOfCurrentStreamPresentationTimesUs; + + @Before + public void setupResultLists() { + presentationTimesUsAvailableForRendering = new ArrayList<>(); + processedTextures = new ArrayList<>(); + renderedPresentationTimesUs = new ArrayList<>(); + endOfCurrentStreamPresentationTimesUs = new ArrayList<>(); + } + + @Before + public void createOutputSurface() { + ImageReader outputImageReader = + ImageReader.newInstance(1, 1, PixelFormat.RGBA_8888, /* maxImages= */ 10); + outputSurfaceInfo = new SurfaceInfo(outputImageReader.getSurface(), 1, 1); + } + + @Before + public void createGlObjects() throws Exception { + VideoFrameProcessingTaskExecutor.ErrorListener errorListener = + exception -> videoFrameProcessingException = exception; + videoFrameProcessingTaskExecutor = + new VideoFrameProcessingTaskExecutor( + newDirectExecutorService(), /* shouldShutdownExecutorService= */ true, errorListener); + + // Ensure the Gl thread is the same as the VideoFrameProcessor thread and the test thread. + eglDisplay = GlUtil.getDefaultEglDisplay(); + eglContext = GlUtil.createEglContext(eglDisplay); + glObjectsProvider = new DefaultGlObjectsProvider(eglContext); + placeholderEglSurface = GlUtil.createFocusedPlaceholderEglSurface(eglContext, eglDisplay); + + inputTextureInfos = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Bitmap bitmap = Bitmap.createBitmap(1, 1, Config.ARGB_8888); + int texId = createTexture(bitmap); + inputTextureInfos.add(new GlTextureInfo(texId, C.INDEX_UNSET, C.INDEX_UNSET, 1, 1)); + } + } + + @Before + public void createListeners() { + videoFrameProcessorListener = + new Listener() { + @Override + public void onOutputFrameAvailableForRendering( + long presentationTimeUs, boolean isRedrawnFrame) { + presentationTimesUsAvailableForRendering.add(presentationTimeUs); + } + }; + inputListener = + new InputListener() { + @Override + public void onInputFrameProcessed(GlTextureInfo inputTexture) { + processedTextures.add(inputTexture); + } + }; + listener = + new FinalShaderProgramWrapper.Listener() { + @Override + public void onInputStreamProcessed() { + int numFramesRendered = renderedPresentationTimesUs.size(); + if (numFramesRendered == 0) { + endOfCurrentStreamPresentationTimesUs.add(C.TIME_UNSET); + } else { + endOfCurrentStreamPresentationTimesUs.add( + renderedPresentationTimesUs.get(numFramesRendered - 1)); + } + } + + @Override + public void onFrameRendered(long presentationTimeUs) { + renderedPresentationTimesUs.add(presentationTimeUs); + } + }; + } + + @After + public void checkExceptionsAndRelease() throws Exception { + if (videoFrameProcessingException != null) { + throw videoFrameProcessingException; + } + // Call release on a new thread as the current thread is the VideoFrameProcessingThread. + getInstrumentation() + .runOnMainSync( + () -> { + try { + videoFrameProcessingTaskExecutor.release( + /* releaseTask= */ () -> { + if (finalShaderProgramWrapper != null) { + finalShaderProgramWrapper.release(); + } + for (GlTextureInfo textureInfo : inputTextureInfos) { + GlUtil.deleteTexture(textureInfo.texId); + } + if (eglContext != null && eglDisplay != null) { + GlUtil.destroyEglContext(eglDisplay, eglContext); + } + }); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + }); + } + + @Test + public void queueInputFrame_surfaceOutputAutomaticFrameRendering_rendersFramesImmediately() + throws Exception { + buildFinalShaderProgramWrapper(/* renderFramesAutomatically= */ true); + + finalShaderProgramWrapper.queueInputFrame(glObjectsProvider, inputTextureInfos.get(0), 1000); + finalShaderProgramWrapper.queueInputFrame(glObjectsProvider, inputTextureInfos.get(1), 2000); + finalShaderProgramWrapper.queueInputFrame(glObjectsProvider, inputTextureInfos.get(2), 3000); + + assertThat(presentationTimesUsAvailableForRendering).containsExactly(1000L, 2000L, 3000L); + assertThat(processedTextures).hasSize(3); + assertThat(renderedPresentationTimesUs).containsExactly(1000L, 2000L, 3000L); + assertThat(endOfCurrentStreamPresentationTimesUs).isEmpty(); + } + + @Test + public void queueInputFrame_surfaceOutputManualFrameRendering_rendersFramesOnRender() + throws Exception { + buildFinalShaderProgramWrapper(/* renderFramesAutomatically= */ false); + + finalShaderProgramWrapper.queueInputFrame(glObjectsProvider, inputTextureInfos.get(0), 1000); + finalShaderProgramWrapper.queueInputFrame(glObjectsProvider, inputTextureInfos.get(1), 2000); + finalShaderProgramWrapper.queueInputFrame(glObjectsProvider, inputTextureInfos.get(2), 3000); + + assertThat(presentationTimesUsAvailableForRendering).containsExactly(1000L, 2000L, 3000L); + assertThat(processedTextures).isEmpty(); + assertThat(renderedPresentationTimesUs).isEmpty(); + assertThat(endOfCurrentStreamPresentationTimesUs).isEmpty(); + + finalShaderProgramWrapper.renderOutputFrame(glObjectsProvider, 10001000); + + assertThat(processedTextures).containsExactly(inputTextureInfos.get(0)); + assertThat(renderedPresentationTimesUs).containsExactly(1000L); + assertThat(endOfCurrentStreamPresentationTimesUs).isEmpty(); + + finalShaderProgramWrapper.renderOutputFrame(glObjectsProvider, 10002000); + + assertThat(processedTextures) + .containsExactly(inputTextureInfos.get(0), inputTextureInfos.get(1)); + assertThat(renderedPresentationTimesUs).containsExactly(1000L, 2000L); + assertThat(endOfCurrentStreamPresentationTimesUs).isEmpty(); + + finalShaderProgramWrapper.renderOutputFrame(glObjectsProvider, 10003000); + + assertThat(processedTextures) + .containsExactly( + inputTextureInfos.get(0), inputTextureInfos.get(1), inputTextureInfos.get(2)); + assertThat(renderedPresentationTimesUs).containsExactly(1000L, 2000L, 3000L); + assertThat(endOfCurrentStreamPresentationTimesUs).isEmpty(); + } + + @Test + public void + signalEndOfCurrentInputStream_surfaceOutputAutomaticFrameRendering_notifiesImmediately() + throws Exception { + buildFinalShaderProgramWrapper(/* renderFramesAutomatically= */ true); + + finalShaderProgramWrapper.queueInputFrame(glObjectsProvider, inputTextureInfos.get(0), 1000); + finalShaderProgramWrapper.queueInputFrame(glObjectsProvider, inputTextureInfos.get(1), 2000); + finalShaderProgramWrapper.queueInputFrame(glObjectsProvider, inputTextureInfos.get(2), 3000); + + assertThat(endOfCurrentStreamPresentationTimesUs).isEmpty(); + + finalShaderProgramWrapper.signalEndOfCurrentInputStream(); + + assertThat(endOfCurrentStreamPresentationTimesUs).containsExactly(3000L); + } + + @Test + public void + signalEndOfCurrentInputStream_surfaceOutputManualFrameRendering_notifiesOnceAllFramesRendered() + throws Exception { + buildFinalShaderProgramWrapper(/* renderFramesAutomatically= */ false); + + finalShaderProgramWrapper.queueInputFrame(glObjectsProvider, inputTextureInfos.get(0), 1000); + finalShaderProgramWrapper.queueInputFrame(glObjectsProvider, inputTextureInfos.get(1), 2000); + finalShaderProgramWrapper.queueInputFrame(glObjectsProvider, inputTextureInfos.get(2), 3000); + finalShaderProgramWrapper.signalEndOfCurrentInputStream(); + finalShaderProgramWrapper.renderOutputFrame(glObjectsProvider, 10001000); + finalShaderProgramWrapper.renderOutputFrame(glObjectsProvider, 10002000); + + assertThat(endOfCurrentStreamPresentationTimesUs).isEmpty(); + + finalShaderProgramWrapper.renderOutputFrame(glObjectsProvider, 10003000); + + assertThat(endOfCurrentStreamPresentationTimesUs).containsExactly(3000L); + } + + private void buildFinalShaderProgramWrapper(boolean renderFramesAutomatically) throws Exception { + finalShaderProgramWrapper = + new FinalShaderProgramWrapper( + getApplicationContext(), + eglDisplay, + eglContext, + placeholderEglSurface, + ColorInfo.SDR_BT709_LIMITED, + videoFrameProcessingTaskExecutor, + directExecutor(), + checkNotNull(videoFrameProcessorListener), + null, + 2, + DefaultVideoFrameProcessor.WORKING_COLOR_SPACE_DEFAULT, + renderFramesAutomatically); + videoFrameProcessingTaskExecutor.invoke( + () -> { + checkNotNull(inputListener); + checkNotNull(listener); + finalShaderProgramWrapper.setInputListener(inputListener); + finalShaderProgramWrapper.setListener(listener); + }); + finalShaderProgramWrapper.setOutputSurfaceInfo(outputSurfaceInfo); + } +}