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);
}
}