From 2642d895bde474fbb888588910249cff6cbbbffc Mon Sep 17 00:00:00 2001 From: kimvde Date: Tue, 25 Mar 2025 07:42:41 -0700 Subject: [PATCH] Remove VideoSink.onRendererEnabled This is part of the effort to make VideoSink independent from renderers. PiperOrigin-RevId: 740344126 --- .../exoplayer/video/DefaultVideoSink.java | 25 +-- .../video/MediaCodecVideoRenderer.java | 26 +++- .../video/PlaybackVideoGraphWrapper.java | 145 +++++++++++------- .../media3/exoplayer/video/VideoSink.java | 21 ++- .../transformer/BufferingVideoSink.java | 15 +- .../transformer/SequenceRenderersFactory.java | 18 ++- .../transformer/BufferingVideoSinkTest.java | 17 +- 7 files changed, 154 insertions(+), 113 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DefaultVideoSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DefaultVideoSink.java index 222fea1031..9a0d6a3e8e 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DefaultVideoSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DefaultVideoSink.java @@ -79,16 +79,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; videoFrameMetadataListener = (presentationTimeUs, releaseTimeNs, format, mediaFormat) -> {}; } - /** - * {@inheritDoc} - * - *

This method will always throw an {@link UnsupportedOperationException}. - */ - @Override - public void onRendererEnabled(boolean mayRenderStartOfStream) { - throw new UnsupportedOperationException(); - } - @Override public void onRendererStarted() { videoFrameReleaseControl.onStarted(); @@ -197,16 +187,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; videoFrameReleaseControl.setChangeFrameRateStrategy(changeFrameRateStrategy); } - /** - * {@inheritDoc} - * - *

This method will always throw an {@link UnsupportedOperationException}. - */ - @Override - public void enableMayRenderStartOfStream() { - throw new UnsupportedOperationException(); - } - /** * {@inheritDoc} * @@ -233,6 +213,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } + @Override + public void allowReleaseFirstFrameBeforeStarted() { + videoFrameReleaseControl.allowReleaseFirstFrameBeforeStarted(); + } + @Override public boolean handleInputFrame( long framePresentationTimeUs, boolean isLastFrame, VideoFrameHandler videoFrameHandler) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java index 4c280b77a4..2afb2858b7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java @@ -190,6 +190,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer private boolean codecHandlesHdr10PlusOutOfBandMetadata; private @MonotonicNonNull VideoSink videoSink; private boolean hasSetVideoSink; + private @VideoSink.FirstFrameReleaseInstruction int nextVideoSinkFirstFrameReleaseInstruction; private @MonotonicNonNull List videoEffects; @Nullable private Surface displaySurface; @Nullable private PlaceholderSurface placeholderSurface; @@ -928,7 +929,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer if (videoEffects != null) { videoSink.setVideoEffects(videoEffects); } - videoSink.onRendererEnabled(mayRenderStartOfStream); + nextVideoSinkFirstFrameReleaseInstruction = + mayRenderStartOfStream + ? RELEASE_FIRST_FRAME_IMMEDIATELY + : RELEASE_FIRST_FRAME_WHEN_STARTED; @Nullable WakeupListener wakeupListener = getWakeupListener(); if (wakeupListener != null) { videoSink.setWakeupListener(wakeupListener); @@ -956,7 +960,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer @Override public void enableMayRenderStartOfStream() { if (videoSink != null) { - videoSink.enableMayRenderStartOfStream(); + if (nextVideoSinkFirstFrameReleaseInstruction == RELEASE_FIRST_FRAME_IMMEDIATELY + || nextVideoSinkFirstFrameReleaseInstruction == RELEASE_FIRST_FRAME_WHEN_STARTED) { + // The first stream change hasn't been queued to the sink. + nextVideoSinkFirstFrameReleaseInstruction = RELEASE_FIRST_FRAME_IMMEDIATELY; + } else { + videoSink.allowReleaseFirstFrameBeforeStarted(); + } } else { videoFrameReleaseControl.allowReleaseFirstFrameBeforeStarted(); } @@ -1642,7 +1652,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer .setWidth(width) .setHeight(height) .setPixelWidthHeightRatio(pixelWidthHeightRatio) - .build()); + .build(), + nextVideoSinkFirstFrameReleaseInstruction); + nextVideoSinkFirstFrameReleaseInstruction = + RELEASE_FIRST_FRAME_WHEN_PREVIOUS_STREAM_PROCESSED; } else { videoFrameReleaseControl.setFrameRate(format.frameRate); } @@ -1656,13 +1669,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer *

The default implementation applies this renderer's video effects. */ protected void changeVideoSinkInputStream( - VideoSink videoSink, @VideoSink.InputType int inputType, Format format) { + VideoSink videoSink, + @VideoSink.InputType int inputType, + Format format, + @VideoSink.FirstFrameReleaseInstruction int firstFrameReleaseInstruction) { List videoEffectsToApply = videoEffects != null ? videoEffects : ImmutableList.of(); videoSink.onInputStreamChanged( inputType, format, getOutputStreamStartPositionUs(), - RELEASE_FIRST_FRAME_WHEN_PREVIOUS_STREAM_PROCESSED, + firstFrameReleaseInstruction, videoEffectsToApply); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java index b4193d10c1..572b7ef26b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java @@ -22,7 +22,6 @@ import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.contains; import static androidx.media3.common.util.Util.getMaxPendingFramesCountForMediaCodecDecoders; import static androidx.media3.exoplayer.video.VideoSink.INPUT_TYPE_SURFACE; -import static androidx.media3.exoplayer.video.VideoSink.RELEASE_FIRST_FRAME_WHEN_PREVIOUS_STREAM_PROCESSED; import static java.lang.annotation.ElementType.TYPE_USE; import android.content.Context; @@ -281,12 +280,6 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video private final Context context; - /** - * A queue of unprocessed input frame start positions. Each position is associated with the - * timestamp from which it should be applied. - */ - private final TimedValueQueue streamStartPositionsUs; - private final VideoGraph.Factory videoGraphFactory; private final SparseArray inputVideoSinks; private final List compositionEffects; @@ -297,12 +290,18 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video private final CopyOnWriteArraySet listeners; private final boolean requestOpenGlToneMapping; + /** + * A queue of unprocessed stream changes. Each stream change is associated with the timestamp from + * which it should be applied. + */ + private TimedValueQueue pendingStreamChanges; + private Format videoGraphOutputFormat; private @MonotonicNonNull HandlerWrapper handler; private @MonotonicNonNull VideoGraph videoGraph; private @MonotonicNonNull VideoFrameMetadataListener videoFrameMetadataListener; private long outputStreamStartPositionUs; - private @VideoSink.FirstFrameReleaseInstruction int nextFirstOutputFrameReleaseInstruction; + private @VideoSink.FirstFrameReleaseInstruction int outputStreamFirstFrameReleaseInstruction; @Nullable private Pair currentSurfaceAndSize; private int pendingFlushCount; private @State int state; @@ -331,7 +330,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video private PlaybackVideoGraphWrapper(Builder builder) { context = builder.context; - streamStartPositionsUs = new TimedValueQueue<>(); + pendingStreamChanges = new TimedValueQueue<>(); videoGraphFactory = checkStateNotNull(builder.videoGraphFactory); inputVideoSinks = new SparseArray<>(); compositionEffects = builder.compositionEffects; @@ -432,13 +431,13 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video // We forward output size changes to the sink even if we are still flushing. videoGraphOutputFormat = videoGraphOutputFormat.buildUpon().setWidth(width).setHeight(height).build(); - onOutputStreamChanged(nextFirstOutputFrameReleaseInstruction); + onOutputStreamChanged(); } @Override public void onOutputFrameRateChanged(float frameRate) { videoGraphOutputFormat = videoGraphOutputFormat.buildUpon().setFrameRate(frameRate).build(); - onOutputStreamChanged(nextFirstOutputFrameReleaseInstruction); + onOutputStreamChanged(); } @Override @@ -469,13 +468,11 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video // The frame presentation time is relative to the start of the Composition and without the // renderer offset lastOutputBufferPresentationTimeUs = bufferPresentationTimeUs; - Long newOutputStreamStartPositionUs = - streamStartPositionsUs.pollFloor(bufferPresentationTimeUs); - if (newOutputStreamStartPositionUs != null - && newOutputStreamStartPositionUs != outputStreamStartPositionUs) { - outputStreamStartPositionUs = newOutputStreamStartPositionUs; - onOutputStreamChanged(nextFirstOutputFrameReleaseInstruction); - nextFirstOutputFrameReleaseInstruction = RELEASE_FIRST_FRAME_WHEN_PREVIOUS_STREAM_PROCESSED; + StreamChangeInfo streamChangeInfo = pendingStreamChanges.pollFloor(bufferPresentationTimeUs); + if (streamChangeInfo != null) { + outputStreamStartPositionUs = streamChangeInfo.startPositionUs; + outputStreamFirstFrameReleaseInstruction = streamChangeInfo.firstFrameReleaseInstruction; + onOutputStreamChanged(); } boolean isLastFrame = finalBufferPresentationTimeUs != C.TIME_UNSET @@ -623,13 +620,15 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video } pendingFlushCount++; defaultVideoSink.flush(resetPosition); - while (streamStartPositionsUs.size() > 1) { - streamStartPositionsUs.pollFirst(); + while (pendingStreamChanges.size() > 1) { + pendingStreamChanges.pollFirst(); } - if (streamStartPositionsUs.size() == 1) { - // Use the latest startPositionUs if none is passed after flushing. - outputStreamStartPositionUs = checkNotNull(streamStartPositionsUs.pollFirst()); - onOutputStreamChanged(nextFirstOutputFrameReleaseInstruction); + if (pendingStreamChanges.size() == 1) { + // Use the latest stream change info if none is passed after flushing. + StreamChangeInfo streamChangeInfo = checkNotNull(pendingStreamChanges.pollFirst()); + outputStreamStartPositionUs = streamChangeInfo.startPositionUs; + outputStreamFirstFrameReleaseInstruction = streamChangeInfo.firstFrameReleaseInstruction; + onOutputStreamChanged(); } lastOutputBufferPresentationTimeUs = C.TIME_UNSET; finalBufferPresentationTimeUs = C.TIME_UNSET; @@ -667,13 +666,12 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video return inputColorInfo; } - private void onOutputStreamChanged( - @VideoSink.FirstFrameReleaseInstruction int firstFrameReleaseInstruction) { + private void onOutputStreamChanged() { defaultVideoSink.onInputStreamChanged( INPUT_TYPE_SURFACE, videoGraphOutputFormat, outputStreamStartPositionUs, - firstFrameReleaseInstruction, + outputStreamFirstFrameReleaseInstruction, /* videoEffects= */ ImmutableList.of()); } @@ -712,14 +710,6 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video listenerExecutor = NO_OP_EXECUTOR; } - @Override - public void onRendererEnabled(boolean mayRenderStartOfStream) { - nextFirstOutputFrameReleaseInstruction = - mayRenderStartOfStream - ? RELEASE_FIRST_FRAME_IMMEDIATELY - : RELEASE_FIRST_FRAME_WHEN_STARTED; - } - @Override public void onRendererStarted() { defaultVideoSink.onRendererStarted(); @@ -808,12 +798,8 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video @FirstFrameReleaseInstruction int firstFrameReleaseInstruction, List videoEffects) { checkState(isInitialized()); - switch (inputType) { - case INPUT_TYPE_SURFACE: - case INPUT_TYPE_BITMAP: - break; - default: - throw new UnsupportedOperationException("Unsupported input type " + inputType); + if (inputType != INPUT_TYPE_SURFACE && inputType != INPUT_TYPE_BITMAP) { + throw new UnsupportedOperationException("Unsupported input type " + inputType); } setPendingVideoEffects(videoEffects); this.inputType = inputType; @@ -822,11 +808,56 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video hasSignaledEndOfCurrentInputStream = false; registerInputStream(format); // Input timestamps should always be positive because they are offset by ExoPlayer. Adding a - // position to the queue with timestamp 0 should therefore always apply it as long as it is - // the only position in the queue. - streamStartPositionsUs.add( - lastBufferPresentationTimeUs == C.TIME_UNSET ? 0 : lastBufferPresentationTimeUs + 1, - startPositionUs); + // stream change info to the queue with timestamp 0 should therefore always apply it as long + // as it is the only one in the queue. + long fromTimestampUs = + lastBufferPresentationTimeUs == C.TIME_UNSET ? 0 : lastBufferPresentationTimeUs + 1; + pendingStreamChanges.add( + fromTimestampUs, + new StreamChangeInfo(startPositionUs, firstFrameReleaseInstruction, fromTimestampUs)); + } + + @Override + public void allowReleaseFirstFrameBeforeStarted() { + // We know that this sink is connected to renderers. Each renderer will first queue a stream + // change that has firstFrameReleaseInstruction set to either RELEASE_FIRST_FRAME_IMMEDIATELY + // or RELEASE_FIRST_FRAME_WHEN_STARTED, and then queue stream changes that have + // firstFrameReleaseInstruction set to RELEASE_FIRST_FRAME_WHEN_PREVIOUS_STREAM_PROCESSED. + // When a renderer queues the first stream change, all previous streams should have been fully + // processed. + // We want to release the first frame immediately if the firstFrameReleaseInstruction of the + // first stream change queued by the current renderer was RELEASE_FIRST_FRAME_WHEN_STARTED and + // the first frame hasn't been released yet. + if (pendingStreamChanges.size() == 0) { + // All the stream changes have already been processed by the VideoGraph. Delegate to the + // downstream component. + defaultVideoSink.allowReleaseFirstFrameBeforeStarted(); + return; + } + TimedValueQueue newPendingStreamChanges = new TimedValueQueue<>(); + boolean isFirstStreamChange = true; + while (pendingStreamChanges.size() > 0) { + StreamChangeInfo streamChangeInfo = checkNotNull(pendingStreamChanges.pollFirst()); + if (isFirstStreamChange) { + if (streamChangeInfo.firstFrameReleaseInstruction == RELEASE_FIRST_FRAME_IMMEDIATELY + || streamChangeInfo.firstFrameReleaseInstruction + == RELEASE_FIRST_FRAME_WHEN_STARTED) { + // The first stream change hasn't been processed by the VideoGraph yet. + streamChangeInfo = + new StreamChangeInfo( + streamChangeInfo.startPositionUs, + RELEASE_FIRST_FRAME_IMMEDIATELY, + streamChangeInfo.fromTimestampUs); + } else { + // The first stream change has already been processed by the VideoGraph. Delegate to the + // downstream component. + defaultVideoSink.allowReleaseFirstFrameBeforeStarted(); + } + isFirstStreamChange = false; + } + newPendingStreamChanges.add(streamChangeInfo.fromTimestampUs, streamChangeInfo); + } + pendingStreamChanges = newPendingStreamChanges; } @Override @@ -883,13 +914,6 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video defaultVideoSink.setChangeFrameRateStrategy(changeFrameRateStrategy); } - @Override - public void enableMayRenderStartOfStream() { - if (nextFirstOutputFrameReleaseInstruction == RELEASE_FIRST_FRAME_WHEN_STARTED) { - nextFirstOutputFrameReleaseInstruction = RELEASE_FIRST_FRAME_IMMEDIATELY; - } - } - @Override public boolean handleInputFrame( long framePresentationTimeUs, boolean isLastFrame, VideoFrameHandler videoFrameHandler) { @@ -1060,6 +1084,21 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video } } + private static final class StreamChangeInfo { + public final long startPositionUs; + public final @VideoSink.FirstFrameReleaseInstruction int firstFrameReleaseInstruction; + public final long fromTimestampUs; + + public StreamChangeInfo( + long startPositionUs, + @VideoSink.FirstFrameReleaseInstruction int firstFrameReleaseInstruction, + long fromTimestampUs) { + this.startPositionUs = startPositionUs; + this.firstFrameReleaseInstruction = firstFrameReleaseInstruction; + this.fromTimestampUs = fromTimestampUs; + } + } + /** Delays reflection for loading a {@link VideoGraph.Factory SingleInputVideoGraph} instance. */ private static final class ReflectiveSingleInputVideoGraphFactory implements VideoGraph.Factory { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoSink.java index 63d20f3220..370b271555 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoSink.java @@ -154,9 +154,6 @@ public interface VideoSink { */ int RELEASE_FIRST_FRAME_WHEN_PREVIOUS_STREAM_PROCESSED = 2; - /** Called when the {@link Renderer} currently feeding this sink is enabled. */ - void onRendererEnabled(boolean mayRenderStartOfStream); - /** Called when the {@link Renderer} currently feeding this sink is started. */ void onRendererStarted(); @@ -261,15 +258,6 @@ public interface VideoSink { */ void setChangeFrameRateStrategy(@C.VideoChangeFrameRateStrategy int changeFrameRateStrategy); - /** - * Enables this video sink to render the start of the stream to its output surface even if the - * renderer is not {@linkplain #onRendererStarted() started} yet. - * - *

This is used to update the value of {@code mayRenderStartOfStream} passed to {@link - * #onRendererEnabled(boolean)}. - */ - void enableMayRenderStartOfStream(); - /** * Informs the video sink that a new input stream will be queued with the given effects. * @@ -290,6 +278,15 @@ public interface VideoSink { @FirstFrameReleaseInstruction int firstFrameReleaseInstruction, List videoEffects); + /** + * Allows the sink to release the first frame even if rendering is not {@linkplain + * #onRendererStarted() started}. + * + *

This is used to update the {@link FirstFrameReleaseInstruction} of the {@linkplain + * #onInputStreamChanged(int, Format, long, int, List) stream} that is currently being processed. + */ + void allowReleaseFirstFrameBeforeStarted(); + /** * Handles a video input frame. * diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/BufferingVideoSink.java b/libraries/transformer/src/main/java/androidx/media3/transformer/BufferingVideoSink.java index 2dd3c7824c..148f29b234 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/BufferingVideoSink.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/BufferingVideoSink.java @@ -81,11 +81,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; pendingOperations.clear(); } - @Override - public void onRendererEnabled(boolean mayRenderStartOfStream) { - executeOrDelay(videoSink -> videoSink.onRendererEnabled(mayRenderStartOfStream)); - } - @Override public void onRendererStarted() { executeOrDelay(VideoSink::onRendererStarted); @@ -213,11 +208,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; executeOrDelay(videoSink -> videoSink.setChangeFrameRateStrategy(changeFrameRateStrategy)); } - @Override - public void enableMayRenderStartOfStream() { - executeOrDelay(VideoSink::enableMayRenderStartOfStream); - } - @Override public void onInputStreamChanged( @InputType int inputType, @@ -231,6 +221,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; inputType, format, startPositionUs, firstFrameReleaseInstruction, videoEffects)); } + @Override + public void allowReleaseFirstFrameBeforeStarted() { + executeOrDelay(VideoSink::allowReleaseFirstFrameBeforeStarted); + } + /** * {@inheritDoc} * diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java index f72e98aae8..c3b967653d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java @@ -24,7 +24,9 @@ import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.SDK_INT; import static androidx.media3.exoplayer.DefaultRenderersFactory.DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS; import static androidx.media3.exoplayer.DefaultRenderersFactory.MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY; +import static androidx.media3.exoplayer.video.VideoSink.RELEASE_FIRST_FRAME_IMMEDIATELY; import static androidx.media3.exoplayer.video.VideoSink.RELEASE_FIRST_FRAME_WHEN_PREVIOUS_STREAM_PROCESSED; +import static androidx.media3.exoplayer.video.VideoSink.RELEASE_FIRST_FRAME_WHEN_STARTED; import android.content.Context; import android.graphics.Bitmap; @@ -435,12 +437,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override protected void changeVideoSinkInputStream( - VideoSink videoSink, @VideoSink.InputType int inputType, Format format) { + VideoSink videoSink, + @VideoSink.InputType int inputType, + Format format, + @VideoSink.FirstFrameReleaseInstruction int firstFrameReleaseInstruction) { videoSink.onInputStreamChanged( inputType, format, getOutputStreamStartPositionUs(), - RELEASE_FIRST_FRAME_WHEN_PREVIOUS_STREAM_PROCESSED, + firstFrameReleaseInstruction, pendingEffects); } @@ -493,6 +498,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private boolean inputStreamPending; private long streamStartPositionUs; private boolean mayRenderStartOfStream; + private @VideoSink.FirstFrameReleaseInstruction int nextFirstFrameReleaseInstruction; private long offsetToCompositionTimeUs; public SequenceImageRenderer( @@ -513,7 +519,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; throws ExoPlaybackException { super.onEnabled(joining, mayRenderStartOfStream); this.mayRenderStartOfStream = mayRenderStartOfStream; - videoSink.onRendererEnabled(mayRenderStartOfStream); + nextFirstFrameReleaseInstruction = + mayRenderStartOfStream + ? RELEASE_FIRST_FRAME_IMMEDIATELY + : RELEASE_FIRST_FRAME_WHEN_STARTED; // TODO: b/328444280 - Do not set a listener on VideoSink, but MediaCodecVideoRenderer must // unregister itself as a listener too. videoSink.setListener(VideoSink.Listener.NO_OP, /* executor= */ (runnable) -> {}); @@ -630,8 +639,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; .setFrameRate(/* frameRate= */ DEFAULT_FRAME_RATE) .build(), streamStartPositionUs, - RELEASE_FIRST_FRAME_WHEN_PREVIOUS_STREAM_PROCESSED, + nextFirstFrameReleaseInstruction, videoEffects); + nextFirstFrameReleaseInstruction = RELEASE_FIRST_FRAME_WHEN_PREVIOUS_STREAM_PROCESSED; inputStreamPending = false; } if (!videoSink.handleInputBitmap(outputImage, checkStateNotNull(timestampIterator))) { diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/BufferingVideoSinkTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/BufferingVideoSinkTest.java index 1c8a87a168..f2d340ceb7 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/BufferingVideoSinkTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/BufferingVideoSinkTest.java @@ -40,26 +40,25 @@ public class BufferingVideoSinkTest { VideoSink videoSinkMock = mock(VideoSink.class); bufferingVideoSink.setVideoSink(videoSinkMock); - bufferingVideoSink.onRendererEnabled(/* mayRenderStartOfStream= */ true); bufferingVideoSink.onRendererStarted(); + bufferingVideoSink.flush(/* resetPosition= */ true); InOrder inOrder = Mockito.inOrder(videoSinkMock); - inOrder.verify(videoSinkMock).onRendererEnabled(/* mayRenderStartOfStream= */ true); inOrder.verify(videoSinkMock).onRendererStarted(); + inOrder.verify(videoSinkMock).flush(/* resetPosition= */ true); } @Test public void setVideoSink_executesPendingOperations() { BufferingVideoSink bufferingVideoSink = new BufferingVideoSink(context); VideoSink videoSinkMock = mock(VideoSink.class); - - bufferingVideoSink.onRendererEnabled(/* mayRenderStartOfStream= */ true); bufferingVideoSink.onRendererStarted(); + bufferingVideoSink.flush(/* resetPosition= */ true); bufferingVideoSink.setVideoSink(videoSinkMock); InOrder inOrder = Mockito.inOrder(videoSinkMock); - inOrder.verify(videoSinkMock).onRendererEnabled(/* mayRenderStartOfStream= */ true); inOrder.verify(videoSinkMock).onRendererStarted(); + inOrder.verify(videoSinkMock).flush(/* resetPosition= */ true); } @Test @@ -69,11 +68,11 @@ public class BufferingVideoSinkTest { bufferingVideoSink.setVideoSink(videoSinkMock); bufferingVideoSink.setVideoSink(null); - bufferingVideoSink.onRendererEnabled(/* mayRenderStartOfStream= */ true); bufferingVideoSink.onRendererStarted(); + bufferingVideoSink.flush(/* resetPosition= */ true); - verify(videoSinkMock, never()).onRendererEnabled(/* mayRenderStartOfStream= */ true); verify(videoSinkMock, never()).onRendererStarted(); + verify(videoSinkMock, never()).flush(/* resetPosition= */ true); } @Test @@ -81,12 +80,12 @@ public class BufferingVideoSinkTest { BufferingVideoSink bufferingVideoSink = new BufferingVideoSink(context); VideoSink videoSinkMock = mock(VideoSink.class); - bufferingVideoSink.onRendererEnabled(/* mayRenderStartOfStream= */ true); bufferingVideoSink.onRendererStarted(); + bufferingVideoSink.flush(/* resetPosition= */ true); bufferingVideoSink.clearPendingOperations(); bufferingVideoSink.setVideoSink(videoSinkMock); - verify(videoSinkMock, never()).onRendererEnabled(/* mayRenderStartOfStream= */ true); verify(videoSinkMock, never()).onRendererStarted(); + verify(videoSinkMock, never()).flush(/* resetPosition= */ true); } }