mirror of
https://github.com/androidx/media.git
synced 2025-05-12 18:19:50 +08:00
Add an AndroidTest for FinalShaderProgramWrapper
PiperOrigin-RevId: 755389859
This commit is contained in:
parent
cc8992410a
commit
faa0644ed1
@ -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}.
|
||||||
|
*
|
||||||
|
* <p>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<GlTextureInfo> inputTextureInfos;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All presentationTimeUs of frames sent to {@link
|
||||||
|
* VideoFrameProcessor.Listener#onOutputFrameAvailableForRendering}
|
||||||
|
*/
|
||||||
|
private @MonotonicNonNull List<Long> presentationTimesUsAvailableForRendering;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All {@link GlTextureInfo} sent to {@link GlShaderProgram.InputListener#onInputFrameProcessed}.
|
||||||
|
*/
|
||||||
|
private @MonotonicNonNull List<GlTextureInfo> processedTextures;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All presentationTimeUs of frames sent to {@link
|
||||||
|
* FinalShaderProgramWrapper.Listener#onFrameRendered}.
|
||||||
|
*/
|
||||||
|
private @MonotonicNonNull List<Long> renderedPresentationTimesUs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of the last presentationTimeUs sent to {@link
|
||||||
|
* FinalShaderProgramWrapper.Listener#onFrameRendered} when {@link
|
||||||
|
* FinalShaderProgramWrapper.Listener#onInputStreamProcessed} is called.
|
||||||
|
*/
|
||||||
|
private @MonotonicNonNull List<Long> 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user