Implement chaining GlTextureProcessor.Listener.

In follow-ups the FrameProcessorChain will set an instance of this
listener for each GlTextureProcessor to chain it with its previous
and next GlTextureProcesssor.

PiperOrigin-RevId: 455628942
This commit is contained in:
hschlueter 2022-06-17 16:57:46 +01:00 committed by Ian Baker
parent 02674f5d3f
commit 981baae709
3 changed files with 272 additions and 2 deletions

View File

@ -0,0 +1,106 @@
/*
* 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 android.util.Pair;
import androidx.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.Queue;
/**
* A {@link GlTextureProcessor.Listener} that connects the {@link GlTextureProcessor} it is
* {@linkplain GlTextureProcessor#setListener(GlTextureProcessor.Listener) set} on to a previous and
* next {@link GlTextureProcessor}.
*/
/* package */ final class ChainingGlTextureProcessorListener
implements GlTextureProcessor.Listener {
@Nullable private final GlTextureProcessor previousGlTextureProcessor;
@Nullable private final GlTextureProcessor nextGlTextureProcessor;
private final FrameProcessingTaskExecutor frameProcessingTaskExecutor;
private final FrameProcessorChain.Listener frameProcessorChainListener;
private final Queue<Pair<TextureInfo, Long>> pendingFrames;
/**
* Creates a new instance.
*
* @param previousGlTextureProcessor The {@link GlTextureProcessor} that comes before the {@link
* GlTextureProcessor} this listener is set on or {@code null} if not applicable.
* @param nextGlTextureProcessor The {@link GlTextureProcessor} that comes after the {@link
* GlTextureProcessor} this listener is set on or {@code null} if not applicable.
* @param frameProcessingTaskExecutor The {@link FrameProcessingTaskExecutor} that is used for
* OpenGL calls. All calls to the previous/next {@link GlTextureProcessor} will be executed by
* the {@link FrameProcessingTaskExecutor}. The caller is responsible for releasing the {@link
* FrameProcessingTaskExecutor}.
* @param frameProcessorChainListener The {@link FrameProcessorChain.Listener} to forward
* exceptions to.
*/
public ChainingGlTextureProcessorListener(
@Nullable GlTextureProcessor previousGlTextureProcessor,
@Nullable GlTextureProcessor nextGlTextureProcessor,
FrameProcessingTaskExecutor frameProcessingTaskExecutor,
FrameProcessorChain.Listener frameProcessorChainListener) {
this.previousGlTextureProcessor = previousGlTextureProcessor;
this.nextGlTextureProcessor = nextGlTextureProcessor;
this.frameProcessingTaskExecutor = frameProcessingTaskExecutor;
this.frameProcessorChainListener = frameProcessorChainListener;
pendingFrames = new ArrayDeque<>();
}
@Override
public void onInputFrameProcessed(TextureInfo inputTexture) {
if (previousGlTextureProcessor != null) {
GlTextureProcessor nonNullPreviousGlTextureProcessor = previousGlTextureProcessor;
frameProcessingTaskExecutor.submit(
() -> nonNullPreviousGlTextureProcessor.releaseOutputFrame(inputTexture));
}
}
@Override
public void onOutputFrameAvailable(TextureInfo outputTexture, long presentationTimeUs) {
if (nextGlTextureProcessor != null) {
GlTextureProcessor nonNullNextGlTextureProcessor = nextGlTextureProcessor;
frameProcessingTaskExecutor.submit(
() -> {
pendingFrames.add(new Pair<>(outputTexture, presentationTimeUs));
processFrameNowOrLater(nonNullNextGlTextureProcessor);
});
}
}
private void processFrameNowOrLater(GlTextureProcessor nextGlTextureProcessor) {
Pair<TextureInfo, Long> pendingFrame = pendingFrames.element();
TextureInfo outputTexture = pendingFrame.first;
long presentationTimeUs = pendingFrame.second;
if (nextGlTextureProcessor.maybeQueueInputFrame(outputTexture, presentationTimeUs)) {
pendingFrames.remove();
} else {
frameProcessingTaskExecutor.submit(() -> processFrameNowOrLater(nextGlTextureProcessor));
}
}
@Override
public void onOutputStreamEnded() {
if (nextGlTextureProcessor != null) {
frameProcessingTaskExecutor.submit(nextGlTextureProcessor::signalEndOfInputStream);
}
}
@Override
public void onFrameProcessingError(FrameProcessingException e) {
frameProcessorChainListener.onFrameProcessingError(e);
}
}

View File

@ -146,11 +146,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
/** /**
* Creates the OpenGL textures and framebuffers, initializes the {@link * Creates the OpenGL context, surfaces, textures, and framebuffers, initializes the {@link
* SingleFrameGlTextureProcessor SingleFrameGlTextureProcessors} corresponding to the {@link * SingleFrameGlTextureProcessor SingleFrameGlTextureProcessors} corresponding to the {@link
* GlEffect GlEffects}, and returns a new {@code FrameProcessorChain}. * GlEffect GlEffects}, and returns a new {@code FrameProcessorChain}.
* *
* <p>This method must be executed using the {@code singleThreadExecutorService}. * <p>This method must be executed using the {@code singleThreadExecutorService}, as all later
* OpenGL commands will be called on that thread.
*/ */
@WorkerThread @WorkerThread
@Nullable @Nullable

View File

@ -0,0 +1,163 @@
/*
* 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 org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import androidx.media3.common.util.Util;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link ChainingGlTextureProcessorListener}. */
@RunWith(AndroidJUnit4.class)
public final class ChainingGlTextureProcessorListenerTest {
private static final long EXECUTOR_WAIT_TIME_MS = 100;
private final FrameProcessorChain.Listener mockFrameProcessorChainListener =
mock(FrameProcessorChain.Listener.class);
private final FrameProcessingTaskExecutor frameProcessingTaskExecutor =
new FrameProcessingTaskExecutor(
Util.newSingleThreadExecutor("Test"), mockFrameProcessorChainListener);
private final GlTextureProcessor mockPreviousGlTextureProcessor = mock(GlTextureProcessor.class);
private final FakeGlTextureProcessor fakeNextGlTextureProcessor =
spy(new FakeGlTextureProcessor());
private final ChainingGlTextureProcessorListener chainingGlTextureProcessorListener =
new ChainingGlTextureProcessorListener(
mockPreviousGlTextureProcessor,
fakeNextGlTextureProcessor,
frameProcessingTaskExecutor,
mockFrameProcessorChainListener);
@After
public void release() throws InterruptedException {
frameProcessingTaskExecutor.release(/* releaseTask= */ () -> {}, EXECUTOR_WAIT_TIME_MS);
}
@Test
public void onFrameProcessingError_callsListener() {
FrameProcessingException exception = new FrameProcessingException("message");
chainingGlTextureProcessorListener.onFrameProcessingError(exception);
verify(mockFrameProcessorChainListener, times(1)).onFrameProcessingError(exception);
}
@Test
public void onInputFrameProcessed_surrendersFrameToPreviousGlTextureProcessor()
throws InterruptedException {
TextureInfo texture =
new TextureInfo(/* texId= */ 1, /* fboId= */ 1, /* width= */ 100, /* height= */ 100);
chainingGlTextureProcessorListener.onInputFrameProcessed(texture);
Thread.sleep(EXECUTOR_WAIT_TIME_MS);
verify(mockPreviousGlTextureProcessor, times(1)).releaseOutputFrame(texture);
}
@Test
public void onOutputFrameAvailable_passesFrameToNextGlTextureProcessor()
throws InterruptedException {
TextureInfo texture =
new TextureInfo(/* texId= */ 1, /* fboId= */ 1, /* width= */ 100, /* height= */ 100);
long presentationTimeUs = 123;
chainingGlTextureProcessorListener.onOutputFrameAvailable(texture, presentationTimeUs);
Thread.sleep(EXECUTOR_WAIT_TIME_MS);
verify(fakeNextGlTextureProcessor, times(1)).maybeQueueInputFrame(texture, presentationTimeUs);
}
@Test
public void onOutputFrameAvailable_nextGlTextureProcessorRejectsFrame_triesAgain()
throws InterruptedException {
TextureInfo texture =
new TextureInfo(/* texId= */ 1, /* fboId= */ 1, /* width= */ 100, /* height= */ 100);
long presentationTimeUs = 123;
fakeNextGlTextureProcessor.rejectNextFrame();
chainingGlTextureProcessorListener.onOutputFrameAvailable(texture, presentationTimeUs);
Thread.sleep(EXECUTOR_WAIT_TIME_MS);
verify(fakeNextGlTextureProcessor, times(2)).maybeQueueInputFrame(texture, presentationTimeUs);
}
@Test
public void onOutputFrameAvailable_twoFramesWithFirstRejected_retriesFirstBeforeSecond()
throws InterruptedException {
TextureInfo firstTexture =
new TextureInfo(/* texId= */ 1, /* fboId= */ 1, /* width= */ 100, /* height= */ 100);
long firstPresentationTimeUs = 123;
TextureInfo secondTexture =
new TextureInfo(/* texId= */ 2, /* fboId= */ 2, /* width= */ 100, /* height= */ 100);
long secondPresentationTimeUs = 567;
fakeNextGlTextureProcessor.rejectNextFrame();
chainingGlTextureProcessorListener.onOutputFrameAvailable(
firstTexture, firstPresentationTimeUs);
chainingGlTextureProcessorListener.onOutputFrameAvailable(
secondTexture, secondPresentationTimeUs);
Thread.sleep(EXECUTOR_WAIT_TIME_MS);
verify(fakeNextGlTextureProcessor, times(2))
.maybeQueueInputFrame(firstTexture, firstPresentationTimeUs);
verify(fakeNextGlTextureProcessor, times(1))
.maybeQueueInputFrame(secondTexture, secondPresentationTimeUs);
}
@Test
public void onOutputStreamEnded_signalsInputStreamEndedToNextGlTextureProcessor()
throws InterruptedException {
chainingGlTextureProcessorListener.onOutputStreamEnded();
Thread.sleep(EXECUTOR_WAIT_TIME_MS);
verify(fakeNextGlTextureProcessor, times(1)).signalEndOfInputStream();
}
private static class FakeGlTextureProcessor implements GlTextureProcessor {
private volatile boolean rejectNextFrame;
public void rejectNextFrame() {
rejectNextFrame = true;
}
@Override
public void setListener(Listener listener) {
throw new UnsupportedOperationException();
}
@Override
public boolean maybeQueueInputFrame(TextureInfo inputTexture, long presentationTimeUs) {
boolean acceptFrame = !rejectNextFrame;
rejectNextFrame = false;
return acceptFrame;
}
@Override
public void releaseOutputFrame(TextureInfo outputTexture) {}
@Override
public void signalEndOfInputStream() {}
@Override
public void release() {}
}
}