diff --git a/library/effect/src/androidTest/java/com/google/android/exoplayer2/effect/GlEffectsFrameProcessorPixelTest.java b/library/effect/src/androidTest/java/com/google/android/exoplayer2/effect/GlEffectsFrameProcessorPixelTest.java index 01747da392..59c951a265 100644 --- a/library/effect/src/androidTest/java/com/google/android/exoplayer2/effect/GlEffectsFrameProcessorPixelTest.java +++ b/library/effect/src/androidTest/java/com/google/android/exoplayer2/effect/GlEffectsFrameProcessorPixelTest.java @@ -145,7 +145,8 @@ public final class GlEffectsFrameProcessorPixelTest { assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); } // TODO(b/262693274): Once texture deletion is added to InternalTextureManager.java, add a test - // queuing multiple input bitmaps to ensure successfully completion without errors. + // queuing multiple input bitmaps to ensure successfully completion without errors, ensuring the + // correct number of frames haas been queued. @Test public void noEffects_withFrameCache_matchesGoldenFile() throws Exception { diff --git a/library/effect/src/main/java/com/google/android/exoplayer2/effect/InternalTextureManager.java b/library/effect/src/main/java/com/google/android/exoplayer2/effect/InternalTextureManager.java index d1b09caf69..148db71bd1 100644 --- a/library/effect/src/main/java/com/google/android/exoplayer2/effect/InternalTextureManager.java +++ b/library/effect/src/main/java/com/google/android/exoplayer2/effect/InternalTextureManager.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2.effect; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static java.lang.Math.round; +import static java.lang.Math.floor; import android.graphics.Bitmap; import android.opengl.GLES20; @@ -29,19 +29,31 @@ import com.google.android.exoplayer2.util.GlUtil; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; -/** Forwards a frame produced from a {@link Bitmap} to a {@link GlShaderProgram} for consumption. */ +/** + * Forwards a frame produced from a {@link Bitmap} to a {@link GlShaderProgram} for consumption + * + *

Methods in this class can be called from any thread. + */ /* package */ class InternalTextureManager implements GlShaderProgram.InputListener { private final GlShaderProgram shaderProgram; private final FrameProcessingTaskExecutor frameProcessingTaskExecutor; + // The queue holds all bitmaps with one or more frames pending to be sent downstream. private final Queue pendingBitmaps; private int downstreamShaderProgramCapacity; - private int availableFrameCount; - + private int framesToQueueForCurrentBitmap; private long currentPresentationTimeUs; - private long totalDurationUs; private boolean inputEnded; + private boolean outputEnded; + /** + * Creates a new instance. + * + * @param shaderProgram The {@link GlShaderProgram} for which this {@code InternalTextureManager} + * will be set as the {@link GlShaderProgram.InputListener}. + * @param frameProcessingTaskExecutor The {@link FrameProcessingTaskExecutor} that the methods of + * this class run on. + */ public InternalTextureManager( GlShaderProgram shaderProgram, FrameProcessingTaskExecutor frameProcessingTaskExecutor) { this.shaderProgram = shaderProgram; @@ -51,6 +63,10 @@ import java.util.concurrent.LinkedBlockingQueue; @Override public void onReadyToAcceptInputFrame() { + // TODO(b/262693274): Delete texture when last duplicate of the frame comes back from the shader + // program and change to only allocate one texId at a time. A change to the + // onInputFrameProcessed() method signature to include presentationTimeUs will probably be + // needed to do this. frameProcessingTaskExecutor.submit( () -> { downstreamShaderProgramCapacity++; @@ -58,70 +74,17 @@ import java.util.concurrent.LinkedBlockingQueue; }); } - @Override - public void onInputFrameProcessed(TextureInfo inputTexture) { - // TODO(b/262693274): Delete texture when last duplicate of the frame comes back from the shader - // program and change to only allocate one texId at a time. A change to method signature to - // include presentationTimeUs will probably be needed to do this. - frameProcessingTaskExecutor.submit( - () -> { - if (availableFrameCount == 0) { - signalEndOfInput(); - } - }); - } - + /** + * Provides an input {@link Bitmap} to put into the video frames. + * + * @see FrameProcessor#queueInputBitmap + */ public void queueInputBitmap( Bitmap inputBitmap, long durationUs, float frameRate, boolean useHdr) { frameProcessingTaskExecutor.submit( () -> setupBitmap(inputBitmap, durationUs, frameRate, useHdr)); } - @WorkerThread - private void setupBitmap(Bitmap bitmap, long durationUs, float frameRate, boolean useHdr) - throws FrameProcessingException { - if (inputEnded) { - return; - } - try { - int bitmapTexId = - GlUtil.createTexture( - bitmap.getWidth(), bitmap.getHeight(), /* useHighPrecisionColorComponents= */ useHdr); - GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, bitmapTexId); - GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, bitmap, /* border= */ 0); - GlUtil.checkGlError(); - - TextureInfo textureInfo = - new TextureInfo( - bitmapTexId, /* fboId= */ C.INDEX_UNSET, bitmap.getWidth(), bitmap.getHeight()); - int timeIncrementUs = round(C.MICROS_PER_SECOND / frameRate); - availableFrameCount += round((frameRate * durationUs) / C.MICROS_PER_SECOND); - totalDurationUs += durationUs; - pendingBitmaps.add( - new BitmapFrameSequenceInfo(textureInfo, timeIncrementUs, totalDurationUs)); - } catch (GlUtil.GlException e) { - throw FrameProcessingException.from(e); - } - maybeQueueToShaderProgram(); - } - - @WorkerThread - private void maybeQueueToShaderProgram() { - if (inputEnded || availableFrameCount == 0 || downstreamShaderProgramCapacity == 0) { - return; - } - availableFrameCount--; - downstreamShaderProgramCapacity--; - - BitmapFrameSequenceInfo currentFrame = checkNotNull(pendingBitmaps.peek()); - shaderProgram.queueInputFrame(currentFrame.textureInfo, currentPresentationTimeUs); - - currentPresentationTimeUs += currentFrame.timeIncrementUs; - if (currentPresentationTimeUs >= currentFrame.endPresentationTimeUs) { - pendingBitmaps.remove(); - } - } - /** * Signals the end of the input. * @@ -130,28 +93,87 @@ import java.util.concurrent.LinkedBlockingQueue; public void signalEndOfInput() { frameProcessingTaskExecutor.submit( () -> { - if (inputEnded) { - return; - } inputEnded = true; - shaderProgram.signalEndOfCurrentInputStream(); + signalEndOfOutput(); }); } + @WorkerThread + private void setupBitmap(Bitmap bitmap, long durationUs, float frameRate, boolean useHdr) + throws FrameProcessingException { + + if (inputEnded) { + return; + } + int bitmapTexId; + try { + bitmapTexId = + GlUtil.createTexture( + bitmap.getWidth(), bitmap.getHeight(), /* useHighPrecisionColorComponents= */ useHdr); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, bitmapTexId); + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, bitmap, /* border= */ 0); + GlUtil.checkGlError(); + } catch (GlUtil.GlException e) { + throw FrameProcessingException.from(e); + } + TextureInfo textureInfo = + new TextureInfo( + bitmapTexId, /* fboId= */ C.INDEX_UNSET, bitmap.getWidth(), bitmap.getHeight()); + int framesToAdd = (int) floor(frameRate * (durationUs / (float) C.MICROS_PER_SECOND)); + long frameDurationUs = (long) floor(C.MICROS_PER_SECOND / frameRate); + pendingBitmaps.add(new BitmapFrameSequenceInfo(textureInfo, frameDurationUs, framesToAdd)); + + maybeQueueToShaderProgram(); + } + + @WorkerThread + private void maybeQueueToShaderProgram() { + if (pendingBitmaps.isEmpty() || downstreamShaderProgramCapacity == 0) { + return; + } + + BitmapFrameSequenceInfo currentBitmap = checkNotNull(pendingBitmaps.peek()); + if (framesToQueueForCurrentBitmap == 0) { + framesToQueueForCurrentBitmap = currentBitmap.numberOfFrames; + } + + framesToQueueForCurrentBitmap--; + downstreamShaderProgramCapacity--; + + shaderProgram.queueInputFrame(currentBitmap.textureInfo, currentPresentationTimeUs); + + currentPresentationTimeUs += currentBitmap.frameDurationUs; + if (framesToQueueForCurrentBitmap == 0) { + pendingBitmaps.remove(); + signalEndOfOutput(); + } + } + + @WorkerThread + private void signalEndOfOutput() { + if (framesToQueueForCurrentBitmap == 0 + && pendingBitmaps.isEmpty() + && inputEnded + && !outputEnded) { + shaderProgram.signalEndOfCurrentInputStream(); + outputEnded = true; + } + } + /** * Value class specifying information to generate all the frames associated with a specific {@link * Bitmap}. */ private static final class BitmapFrameSequenceInfo { public final TextureInfo textureInfo; - public final long timeIncrementUs; - public final long endPresentationTimeUs; + public final long frameDurationUs; + public final int numberOfFrames; public BitmapFrameSequenceInfo( - TextureInfo textureInfo, long timeIncrementUs, long endPresentationTimeUs) { + TextureInfo textureInfo, long frameDurationUs, int numberOfFrames) { this.textureInfo = textureInfo; - this.timeIncrementUs = timeIncrementUs; - this.endPresentationTimeUs = endPresentationTimeUs; + this.frameDurationUs = frameDurationUs; + this.numberOfFrames = numberOfFrames; } } }