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:
parent
02674f5d3f
commit
981baae709
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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() {}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user