Reduce flakiness of GL release test

Based on experimentation it seems that buffers can occasionally (roughly 1% of test runs) be dropped when rendering off-screen from EGL on the emulator. Specifically, in this test, sometimes after rendering three buffers with distinct timestamps only the first and third buffers' timestamps are handled in the `ImageReader`'s image available callback causing the assertion checking all frames rendered to fail. This behavior seems to be independent of the nanosecond presentation time attached to the buffers (as expected for off-screen rendering).

Introducing a pause of 1 second between rendering each frame reduces the flake rate to around 1/2000. This increases the run time of some of the tests, so this change also removes the 5 second `FRAME_PROCESSING_WAIT_MS` (it seems to be unnecessary when rendering off-screen) and instead uses a latch to wait until the frame processor has handled 'end of stream'.

PiperOrigin-RevId: 499440591
This commit is contained in:
andrewlewis 2023-01-04 11:17:15 +00:00 committed by Marc Baechinger
parent fed93723a3
commit ef016832b2
2 changed files with 141 additions and 95 deletions

View File

@ -18,10 +18,12 @@ package androidx.media3.effect;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.graphics.PixelFormat; import android.graphics.PixelFormat;
import android.media.Image; import android.media.Image;
import android.media.ImageReader; import android.media.ImageReader;
import androidx.annotation.Nullable;
import androidx.media3.common.ColorInfo; import androidx.media3.common.ColorInfo;
import androidx.media3.common.DebugViewProvider; import androidx.media3.common.DebugViewProvider;
import androidx.media3.common.FrameInfo; import androidx.media3.common.FrameInfo;
@ -32,14 +34,16 @@ import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
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 com.google.common.primitives.Longs;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Queue; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.junit.After; import org.junit.After;
@ -52,16 +56,19 @@ public final class GlEffectsFrameProcessorFrameReleaseTest {
private static final int WIDTH = 200; private static final int WIDTH = 200;
private static final int HEIGHT = 100; private static final int HEIGHT = 100;
private static final long FRAME_PROCESSING_WAIT_MS = 5000L; /**
private static final long MILLIS_TO_NANOS = 1_000_000L; * Time to wait between releasing frames to avoid frame drops between GL and the {@link
* ImageReader}.
*/
private static final long PER_FRAME_RELEASE_WAIT_TIME_MS = 1000L;
/** Maximum time to wait for each released frame to be notified. */
private static final long PER_FRAME_TIMEOUT_MS = 5000L;
private static final long MICROS_TO_NANOS = 1000L; private static final long MICROS_TO_NANOS = 1000L;
private final AtomicReference<FrameProcessingException> frameProcessingException = private final LinkedBlockingQueue<Long> outputReleaseTimesNs = new LinkedBlockingQueue<>();
new AtomicReference<>();
private final Queue<Long> outputReleaseTimesNs = new ConcurrentLinkedQueue<>();
private @MonotonicNonNull GlEffectsFrameProcessor glEffectsFrameProcessor; private @MonotonicNonNull GlEffectsFrameProcessor glEffectsFrameProcessor;
private volatile @MonotonicNonNull Runnable produceBlankFramesTask;
@After @After
public void release() { public void release() {
@ -74,53 +81,58 @@ public final class GlEffectsFrameProcessorFrameReleaseTest {
public void automaticFrameRelease_withOneFrame_reusesInputTimestamp() throws Exception { public void automaticFrameRelease_withOneFrame_reusesInputTimestamp() throws Exception {
long originalPresentationTimeUs = 1234; long originalPresentationTimeUs = 1234;
AtomicLong actualPresentationTimeUs = new AtomicLong(); AtomicLong actualPresentationTimeUs = new AtomicLong();
setupGlEffectsFrameProcessorWithBlankFrameProducer( processFramesToEndOfStream(
/* inputPresentationTimesUs= */ new long[] {originalPresentationTimeUs}, /* inputPresentationTimesUs= */ new long[] {originalPresentationTimeUs},
/* onFrameAvailableListener= */ actualPresentationTimeUs::set, /* onFrameAvailableListener= */ actualPresentationTimeUs::set,
/* releaseFramesAutomatically= */ true); /* releaseFramesAutomatically= */ true);
checkNotNull(produceBlankFramesTask).run();
Thread.sleep(FRAME_PROCESSING_WAIT_MS);
assertThat(frameProcessingException.get()).isNull();
assertThat(actualPresentationTimeUs.get()).isEqualTo(originalPresentationTimeUs); assertThat(actualPresentationTimeUs.get()).isEqualTo(originalPresentationTimeUs);
assertThat(outputReleaseTimesNs).containsExactly(MICROS_TO_NANOS * originalPresentationTimeUs); ImmutableList<Long> actualReleaseTimesNs =
waitForFrameReleaseAndGetReleaseTimesNs(/* expectedFrameCount= */ 1);
assertThat(actualReleaseTimesNs).containsExactly(MICROS_TO_NANOS * originalPresentationTimeUs);
} }
@Test @Test
public void automaticFrameRelease_withThreeFrames_reusesInputTimestamps() throws Exception { public void automaticFrameRelease_withThreeFrames_reusesInputTimestamps() throws Exception {
long[] originalPresentationTimesUs = new long[] {1234, 3456, 4567}; long[] originalPresentationTimesUs = new long[] {1234, 3456, 4567};
ArrayList<Long> actualPresentationTimesUs = new ArrayList<>(); ArrayList<Long> actualPresentationTimesUs = new ArrayList<>();
setupGlEffectsFrameProcessorWithBlankFrameProducer( processFramesToEndOfStream(
originalPresentationTimesUs, originalPresentationTimesUs,
/* onFrameAvailableListener= */ actualPresentationTimesUs::add, /* onFrameAvailableListener= */ presentationTimeUs -> {
actualPresentationTimesUs.add(presentationTimeUs);
try {
// TODO(b/264252759): Investigate output frames being dropped and remove sleep.
// Frames can be dropped silently between EGL and the ImageReader. Sleep after each call
// to swap buffers, to avoid this behavior.
Thread.sleep(PER_FRAME_RELEASE_WAIT_TIME_MS);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
},
/* releaseFramesAutomatically= */ true); /* releaseFramesAutomatically= */ true);
checkNotNull(produceBlankFramesTask).run();
Thread.sleep(FRAME_PROCESSING_WAIT_MS);
assertThat(frameProcessingException.get()).isNull();
assertThat(actualPresentationTimesUs) assertThat(actualPresentationTimesUs)
.containsExactly( .containsExactly(
originalPresentationTimesUs[0], originalPresentationTimesUs[0],
originalPresentationTimesUs[1], originalPresentationTimesUs[1],
originalPresentationTimesUs[2]) originalPresentationTimesUs[2])
.inOrder(); .inOrder();
assertThat(outputReleaseTimesNs) ImmutableList<Long> actualReleaseTimesNs =
waitForFrameReleaseAndGetReleaseTimesNs(/* expectedFrameCount= */ 3);
assertThat(actualReleaseTimesNs)
.containsExactly( .containsExactly(
MICROS_TO_NANOS * originalPresentationTimesUs[0], MICROS_TO_NANOS * originalPresentationTimesUs[0],
MICROS_TO_NANOS * originalPresentationTimesUs[1], MICROS_TO_NANOS * originalPresentationTimesUs[1],
MICROS_TO_NANOS * originalPresentationTimesUs[2]) MICROS_TO_NANOS * originalPresentationTimesUs[2])
.inOrder(); .inOrder();
;
} }
@Test @Test
public void controlledFrameRelease_withOneFrame_usesGivenTimestamp() throws Exception { public void controlledFrameRelease_withOneFrame_usesGivenTimestamp() throws Exception {
long originalPresentationTimeUs = 1234; long originalPresentationTimeUs = 1234;
long releaseTimesNs = System.nanoTime() + MILLIS_TO_NANOS * FRAME_PROCESSING_WAIT_MS + 345678; long releaseTimesNs = System.nanoTime() + 345678;
AtomicLong actualPresentationTimeUs = new AtomicLong(); AtomicLong actualPresentationTimeUs = new AtomicLong();
setupGlEffectsFrameProcessorWithBlankFrameProducer( processFramesToEndOfStream(
/* inputPresentationTimesUs= */ new long[] {originalPresentationTimeUs}, /* inputPresentationTimesUs= */ new long[] {originalPresentationTimeUs},
/* onFrameAvailableListener= */ presentationTimeUs -> { /* onFrameAvailableListener= */ presentationTimeUs -> {
actualPresentationTimeUs.set(presentationTimeUs); actualPresentationTimeUs.set(presentationTimeUs);
@ -128,12 +140,9 @@ public final class GlEffectsFrameProcessorFrameReleaseTest {
}, },
/* releaseFramesAutomatically= */ false); /* releaseFramesAutomatically= */ false);
checkNotNull(produceBlankFramesTask).run(); ImmutableList<Long> actualReleaseTimesNs =
Thread.sleep(FRAME_PROCESSING_WAIT_MS); waitForFrameReleaseAndGetReleaseTimesNs(/* expectedFrameCount= */ 1);
assertThat(actualReleaseTimesNs).containsExactly(releaseTimesNs);
assertThat(frameProcessingException.get()).isNull();
assertThat(actualPresentationTimeUs.get()).isEqualTo(originalPresentationTimeUs);
assertThat(outputReleaseTimesNs).containsExactly(releaseTimesNs);
} }
@Test @Test
@ -142,7 +151,7 @@ public final class GlEffectsFrameProcessorFrameReleaseTest {
long originalPresentationTimeUs = 1234; long originalPresentationTimeUs = 1234;
long releaseTimesNs = FrameProcessor.RELEASE_OUTPUT_FRAME_IMMEDIATELY; long releaseTimesNs = FrameProcessor.RELEASE_OUTPUT_FRAME_IMMEDIATELY;
AtomicLong actualPresentationTimeUs = new AtomicLong(); AtomicLong actualPresentationTimeUs = new AtomicLong();
setupGlEffectsFrameProcessorWithBlankFrameProducer( processFramesToEndOfStream(
/* inputPresentationTimesUs= */ new long[] {originalPresentationTimeUs}, /* inputPresentationTimesUs= */ new long[] {originalPresentationTimeUs},
/* onFrameAvailableListener= */ presentationTimeUs -> { /* onFrameAvailableListener= */ presentationTimeUs -> {
actualPresentationTimeUs.set(presentationTimeUs); actualPresentationTimeUs.set(presentationTimeUs);
@ -150,13 +159,11 @@ public final class GlEffectsFrameProcessorFrameReleaseTest {
}, },
/* releaseFramesAutomatically= */ false); /* releaseFramesAutomatically= */ false);
checkNotNull(produceBlankFramesTask).run();
Thread.sleep(FRAME_PROCESSING_WAIT_MS);
assertThat(frameProcessingException.get()).isNull();
assertThat(actualPresentationTimeUs.get()).isEqualTo(originalPresentationTimeUs); assertThat(actualPresentationTimeUs.get()).isEqualTo(originalPresentationTimeUs);
// The actual release time is determined by the FrameProcessor when releasing the frame. // The actual release time is determined by the FrameProcessor when releasing the frame.
assertThat(outputReleaseTimesNs).hasSize(1); ImmutableList<Long> actualReleaseTimesNs =
waitForFrameReleaseAndGetReleaseTimesNs(/* expectedFrameCount= */ 1);
assertThat(actualReleaseTimesNs).hasSize(1);
} }
@Test @Test
@ -164,7 +171,7 @@ public final class GlEffectsFrameProcessorFrameReleaseTest {
long originalPresentationTimeUs = 1234; long originalPresentationTimeUs = 1234;
long releaseTimeBeforeCurrentTimeNs = System.nanoTime() - 345678; long releaseTimeBeforeCurrentTimeNs = System.nanoTime() - 345678;
AtomicLong actualPresentationTimeUs = new AtomicLong(); AtomicLong actualPresentationTimeUs = new AtomicLong();
setupGlEffectsFrameProcessorWithBlankFrameProducer( processFramesToEndOfStream(
/* inputPresentationTimesUs= */ new long[] {originalPresentationTimeUs}, /* inputPresentationTimesUs= */ new long[] {originalPresentationTimeUs},
/* onFrameAvailableListener= */ presentationTimeUs -> { /* onFrameAvailableListener= */ presentationTimeUs -> {
actualPresentationTimeUs.set(presentationTimeUs); actualPresentationTimeUs.set(presentationTimeUs);
@ -172,21 +179,18 @@ public final class GlEffectsFrameProcessorFrameReleaseTest {
}, },
/* releaseFramesAutomatically= */ false); /* releaseFramesAutomatically= */ false);
checkNotNull(produceBlankFramesTask).run(); ImmutableList<Long> actualReleaseTimesNs =
Thread.sleep(FRAME_PROCESSING_WAIT_MS); waitForFrameReleaseAndGetReleaseTimesNs(/* expectedFrameCount= */ 1);
assertThat(actualReleaseTimesNs).hasSize(1);
assertThat(frameProcessingException.get()).isNull();
assertThat(actualPresentationTimeUs.get()).isEqualTo(originalPresentationTimeUs);
assertThat(outputReleaseTimesNs).hasSize(1);
// The actual release time is determined by the FrameProcessor when releasing the frame. // The actual release time is determined by the FrameProcessor when releasing the frame.
assertThat(outputReleaseTimesNs.remove()).isAtLeast(releaseTimeBeforeCurrentTimeNs); assertThat(actualReleaseTimesNs.get(0)).isAtLeast(releaseTimeBeforeCurrentTimeNs);
} }
@Test @Test
public void controlledFrameRelease_requestsFrameDropping_dropsFrame() throws Exception { public void controlledFrameRelease_requestsFrameDropping_dropsFrame() throws Exception {
long originalPresentationTimeUs = 1234; long originalPresentationTimeUs = 1234;
AtomicLong actualPresentationTimeUs = new AtomicLong(); AtomicLong actualPresentationTimeUs = new AtomicLong();
setupGlEffectsFrameProcessorWithBlankFrameProducer( processFramesToEndOfStream(
/* inputPresentationTimesUs= */ new long[] {originalPresentationTimeUs}, /* inputPresentationTimesUs= */ new long[] {originalPresentationTimeUs},
/* onFrameAvailableListener= */ presentationTimeNs -> { /* onFrameAvailableListener= */ presentationTimeNs -> {
actualPresentationTimeUs.set(presentationTimeNs); actualPresentationTimeUs.set(presentationTimeNs);
@ -195,75 +199,77 @@ public final class GlEffectsFrameProcessorFrameReleaseTest {
}, },
/* releaseFramesAutomatically= */ false); /* releaseFramesAutomatically= */ false);
checkNotNull(produceBlankFramesTask).run(); waitForFrameReleaseAndGetReleaseTimesNs(/* expectedFrameCount= */ 0);
Thread.sleep(FRAME_PROCESSING_WAIT_MS);
assertThat(frameProcessingException.get()).isNull();
assertThat(actualPresentationTimeUs.get()).isEqualTo(originalPresentationTimeUs);
assertThat(outputReleaseTimesNs).isEmpty();
} }
@Test @Test
public void controlledFrameRelease_withThreeIndividualFrames_usesGivenTimestamps() public void controlledFrameRelease_withThreeIndividualFrames_usesGivenTimestamps()
throws Exception { throws Exception {
long[] originalPresentationTimesUs = new long[] {1234, 3456, 4567}; long[] originalPresentationTimesUs = new long[] {1234, 3456, 4567};
long offsetNs = System.nanoTime() + MILLIS_TO_NANOS * FRAME_PROCESSING_WAIT_MS; long offsetNs = System.nanoTime();
long[] releaseTimesNs = new long[] {offsetNs + 123456, offsetNs + 234567, offsetNs + 345678}; long[] releaseTimesNs = new long[] {offsetNs + 123456, offsetNs + 234567, offsetNs + 345678};
ArrayList<Long> actualPresentationTimesUs = new ArrayList<>(); ArrayList<Long> actualPresentationTimesUs = new ArrayList<>();
AtomicInteger frameIndex = new AtomicInteger(); AtomicInteger frameIndex = new AtomicInteger();
setupGlEffectsFrameProcessorWithBlankFrameProducer( processFramesToEndOfStream(
/* inputPresentationTimesUs= */ originalPresentationTimesUs, /* inputPresentationTimesUs= */ originalPresentationTimesUs,
/* onFrameAvailableListener= */ presentationTimeUs -> { /* onFrameAvailableListener= */ presentationTimeUs -> {
actualPresentationTimesUs.add(presentationTimeUs); actualPresentationTimesUs.add(presentationTimeUs);
checkNotNull(glEffectsFrameProcessor) checkNotNull(glEffectsFrameProcessor)
.releaseOutputFrame(releaseTimesNs[frameIndex.getAndIncrement()]); .releaseOutputFrame(releaseTimesNs[frameIndex.getAndIncrement()]);
try {
// TODO(b/264252759): Investigate output frames being dropped and remove sleep.
// Frames can be dropped silently between EGL and the ImageReader. Sleep after each call
// to swap buffers, to avoid this behavior.
Thread.sleep(PER_FRAME_RELEASE_WAIT_TIME_MS);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}, },
/* releaseFramesAutomatically= */ false); /* releaseFramesAutomatically= */ false);
checkNotNull(produceBlankFramesTask).run();
Thread.sleep(FRAME_PROCESSING_WAIT_MS);
assertThat(frameProcessingException.get()).isNull();
assertThat(actualPresentationTimesUs) assertThat(actualPresentationTimesUs)
.containsExactly( .containsExactly(
originalPresentationTimesUs[0], originalPresentationTimesUs[0],
originalPresentationTimesUs[1], originalPresentationTimesUs[1],
originalPresentationTimesUs[2]) originalPresentationTimesUs[2])
.inOrder(); .inOrder();
assertThat(frameIndex.get()).isEqualTo(originalPresentationTimesUs.length); int actualFrameCount = frameIndex.get();
assertThat(outputReleaseTimesNs) assertThat(actualFrameCount).isEqualTo(originalPresentationTimesUs.length);
.containsExactly(releaseTimesNs[0], releaseTimesNs[1], releaseTimesNs[2]) long[] actualReleaseTimesNs =
.inOrder(); Longs.toArray(waitForFrameReleaseAndGetReleaseTimesNs(actualFrameCount));
assertThat(actualReleaseTimesNs).isEqualTo(releaseTimesNs);
} }
@Test @Test
public void controlledFrameRelease_withThreeFramesAtOnce_usesGivenTimestamps() throws Exception { public void controlledFrameRelease_withThreeFramesAtOnce_usesGivenTimestamps() throws Exception {
long[] originalPresentationTimesUs = new long[] {1234, 3456, 4567}; long[] originalPresentationTimesUs = new long[] {1234, 3456, 4567};
long offsetNs = System.nanoTime() + MILLIS_TO_NANOS * 2 * FRAME_PROCESSING_WAIT_MS; long offsetNs = System.nanoTime();
long[] releaseTimesNs = new long[] {offsetNs + 123456, offsetNs + 234567, offsetNs + 345678}; long[] releaseTimesNs = new long[] {offsetNs + 123456, offsetNs + 234567, offsetNs + 345678};
ArrayList<Long> actualPresentationTimesUs = new ArrayList<>(); ArrayList<Long> actualPresentationTimesUs = new ArrayList<>();
setupGlEffectsFrameProcessorWithBlankFrameProducer( processFramesToEndOfStream(
/* inputPresentationTimesUs= */ originalPresentationTimesUs, /* inputPresentationTimesUs= */ originalPresentationTimesUs,
/* onFrameAvailableListener= */ actualPresentationTimesUs::add, /* onFrameAvailableListener= */ actualPresentationTimesUs::add,
/* releaseFramesAutomatically= */ false); /* releaseFramesAutomatically= */ false);
checkNotNull(produceBlankFramesTask).run(); // TODO(b/264252759): Investigate output frames being dropped and remove sleep.
Thread.sleep(FRAME_PROCESSING_WAIT_MS); // Frames can be dropped silently between EGL and the ImageReader. Sleep after each call
// to swap buffers, to avoid this behavior.
glEffectsFrameProcessor.releaseOutputFrame(releaseTimesNs[0]); glEffectsFrameProcessor.releaseOutputFrame(releaseTimesNs[0]);
Thread.sleep(PER_FRAME_RELEASE_WAIT_TIME_MS);
glEffectsFrameProcessor.releaseOutputFrame(releaseTimesNs[1]); glEffectsFrameProcessor.releaseOutputFrame(releaseTimesNs[1]);
Thread.sleep(PER_FRAME_RELEASE_WAIT_TIME_MS);
glEffectsFrameProcessor.releaseOutputFrame(releaseTimesNs[2]); glEffectsFrameProcessor.releaseOutputFrame(releaseTimesNs[2]);
Thread.sleep(FRAME_PROCESSING_WAIT_MS); Thread.sleep(PER_FRAME_RELEASE_WAIT_TIME_MS);
assertThat(frameProcessingException.get()).isNull();
assertThat(actualPresentationTimesUs) assertThat(actualPresentationTimesUs)
.containsExactly( .containsExactly(
originalPresentationTimesUs[0], originalPresentationTimesUs[0],
originalPresentationTimesUs[1], originalPresentationTimesUs[1],
originalPresentationTimesUs[2]) originalPresentationTimesUs[2])
.inOrder(); .inOrder();
assertThat(outputReleaseTimesNs) long[] actualReleaseTimesNs =
.containsExactly(releaseTimesNs[0], releaseTimesNs[1], releaseTimesNs[2]) Longs.toArray(waitForFrameReleaseAndGetReleaseTimesNs(/* expectedFrameCount= */ 3));
.inOrder(); assertThat(actualReleaseTimesNs).isEqualTo(releaseTimesNs);
} }
private interface OnFrameAvailableListener { private interface OnFrameAvailableListener {
@ -271,20 +277,21 @@ public final class GlEffectsFrameProcessorFrameReleaseTest {
} }
@EnsuresNonNull("glEffectsFrameProcessor") @EnsuresNonNull("glEffectsFrameProcessor")
private void setupGlEffectsFrameProcessorWithBlankFrameProducer( private void processFramesToEndOfStream(
long[] inputPresentationTimesUs, long[] inputPresentationTimesUs,
OnFrameAvailableListener onFrameAvailableListener, OnFrameAvailableListener onFrameAvailableListener,
boolean releaseFramesAutomatically) boolean releaseFramesAutomatically)
throws Exception { throws Exception {
AtomicReference<@NullableType FrameProcessingException> frameProcessingExceptionReference =
new AtomicReference<>();
BlankFrameProducer blankFrameProducer = new BlankFrameProducer();
CountDownLatch frameProcessingEndedCountDownLatch = new CountDownLatch(1);
glEffectsFrameProcessor = glEffectsFrameProcessor =
checkNotNull( checkNotNull(
new GlEffectsFrameProcessor.Factory() new GlEffectsFrameProcessor.Factory()
.create( .create(
getApplicationContext(), getApplicationContext(),
ImmutableList.of( ImmutableList.of((GlEffect) (context, useHdr) -> blankFrameProducer),
(GlEffect)
(context, useHdr) ->
new BlankFrameProducer(inputPresentationTimesUs, useHdr)),
DebugViewProvider.NONE, DebugViewProvider.NONE,
/* inputColorInfo= */ ColorInfo.SDR_BT709_LIMITED, /* inputColorInfo= */ ColorInfo.SDR_BT709_LIMITED,
/* outputColorInfo= */ ColorInfo.SDR_BT709_LIMITED, /* outputColorInfo= */ ColorInfo.SDR_BT709_LIMITED,
@ -318,31 +325,60 @@ public final class GlEffectsFrameProcessorFrameReleaseTest {
@Override @Override
public void onFrameProcessingError(FrameProcessingException exception) { public void onFrameProcessingError(FrameProcessingException exception) {
frameProcessingException.set(exception); frameProcessingExceptionReference.set(exception);
frameProcessingEndedCountDownLatch.countDown();
} }
@Override @Override
public void onFrameProcessingEnded() {} public void onFrameProcessingEnded() {
frameProcessingEndedCountDownLatch.countDown();
}
})); }));
glEffectsFrameProcessor.setInputFrameInfo( glEffectsFrameProcessor
new FrameInfo(WIDTH, HEIGHT, /* pixelWidthHeightRatio= */ 1, /* streamOffsetUs= */ 0)); .getTaskExecutor()
// A frame needs to be registered despite not queuing any external input to ensure that the .submit(
// frame processor knows about the stream offset. () -> {
glEffectsFrameProcessor.registerInputFrame(); blankFrameProducer.configureGlObjects();
checkNotNull(glEffectsFrameProcessor)
.setInputFrameInfo(
new FrameInfo(
WIDTH, HEIGHT, /* pixelWidthHeightRatio= */ 1, /* streamOffsetUs= */ 0));
// A frame needs to be registered despite not queuing any external input to ensure
// that
// the frame processor knows about the stream offset.
glEffectsFrameProcessor.registerInputFrame();
blankFrameProducer.produceBlankFramesAndQueueEndOfStream(inputPresentationTimesUs);
});
frameProcessingEndedCountDownLatch.await();
@Nullable Exception frameProcessingException = frameProcessingExceptionReference.get();
if (frameProcessingException != null) {
throw frameProcessingException;
}
}
private ImmutableList<Long> waitForFrameReleaseAndGetReleaseTimesNs(int expectedFrameCount)
throws Exception {
ImmutableList.Builder<Long> listBuilder = new ImmutableList.Builder<>();
for (int i = 0; i < expectedFrameCount; i++) {
listBuilder.add(checkNotNull(outputReleaseTimesNs.poll(PER_FRAME_TIMEOUT_MS, MILLISECONDS)));
}
// This is a best-effort check because there's no guarantee that frames aren't added to the
// release times after this method has been called.
assertThat(outputReleaseTimesNs).isEmpty();
return listBuilder.build();
} }
/** Produces blank frames with the given timestamps. */ /** Produces blank frames with the given timestamps. */
private final class BlankFrameProducer implements GlTextureProcessor { private static final class BlankFrameProducer implements GlTextureProcessor {
private final TextureInfo blankTexture; private @MonotonicNonNull TextureInfo blankTexture;
private final long[] presentationTimesUs; private @MonotonicNonNull OutputListener outputListener;
public BlankFrameProducer(long[] presentationTimesUs, boolean useHdr) public void configureGlObjects() throws FrameProcessingException {
throws FrameProcessingException {
this.presentationTimesUs = presentationTimesUs;
try { try {
int texId = GlUtil.createTexture(WIDTH, HEIGHT, useHdr); int texId =
GlUtil.createTexture(WIDTH, HEIGHT, /* useHighPrecisionColorComponents= */ false);
int fboId = GlUtil.createFboForTexture(texId); int fboId = GlUtil.createFboForTexture(texId);
blankTexture = new TextureInfo(texId, fboId, WIDTH, HEIGHT); blankTexture = new TextureInfo(texId, fboId, WIDTH, HEIGHT);
GlUtil.focusFramebufferUsingCurrentContext(fboId, WIDTH, HEIGHT); GlUtil.focusFramebufferUsingCurrentContext(fboId, WIDTH, HEIGHT);
@ -352,17 +388,20 @@ public final class GlEffectsFrameProcessorFrameReleaseTest {
} }
} }
public void produceBlankFramesAndQueueEndOfStream(long[] presentationTimesUs) {
checkNotNull(outputListener);
for (long presentationTimeUs : presentationTimesUs) {
outputListener.onOutputFrameAvailable(checkNotNull(blankTexture), presentationTimeUs);
}
outputListener.onCurrentOutputStreamEnded();
}
@Override @Override
public void setInputListener(InputListener inputListener) {} public void setInputListener(InputListener inputListener) {}
@Override @Override
public void setOutputListener(OutputListener outputListener) { public void setOutputListener(OutputListener outputListener) {
produceBlankFramesTask = this.outputListener = outputListener;
() -> {
for (long presentationTimeUs : presentationTimesUs) {
outputListener.onOutputFrameAvailable(blankTexture, presentationTimeUs);
}
};
} }
@Override @Override

View File

@ -25,6 +25,7 @@ import android.opengl.EGLContext;
import android.opengl.EGLDisplay; import android.opengl.EGLDisplay;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.ColorInfo; import androidx.media3.common.ColorInfo;
@ -368,6 +369,12 @@ public final class GlEffectsFrameProcessor implements FrameProcessor {
previousStreamOffsetUs = C.TIME_UNSET; previousStreamOffsetUs = C.TIME_UNSET;
} }
/** Returns the task executor that runs frame processing tasks. */
@VisibleForTesting
/* package */ FrameProcessingTaskExecutor getTaskExecutor() {
return frameProcessingTaskExecutor;
}
@Override @Override
public Surface getInputSurface() { public Surface getInputSurface() {
return inputSurface; return inputSurface;