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 6d8fc46e60..35b0f8e0ff 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 @@ -177,15 +177,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; throw new UnsupportedOperationException(); } - @Override - public void setStreamStartPositionUs(long streamStartPositionUs) { - if (streamStartPositionUs != this.streamStartPositionUs) { - videoFrameRenderControl.onStreamChanged( - RELEASE_FIRST_FRAME_WHEN_PREVIOUS_STREAM_PROCESSED, streamStartPositionUs); - this.streamStartPositionUs = streamStartPositionUs; - } - } - @Override public void setBufferTimestampAdjustmentUs(long bufferTimestampAdjustmentUs) { this.bufferTimestampAdjustmentUs = bufferTimestampAdjustmentUs; @@ -220,7 +211,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ @Override public void onInputStreamChanged( - @InputType int inputType, Format format, List videoEffects) { + @InputType int inputType, Format format, long startPositionUs, List videoEffects) { checkState(videoEffects.isEmpty()); if (format.width != inputFormat.width || format.height != inputFormat.height) { videoFrameRenderControl.onVideoSizeChanged(format.width, format.height); @@ -229,6 +220,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; videoFrameReleaseControl.setFrameRate(format.frameRate); } inputFormat = format; + if (startPositionUs != this.streamStartPositionUs) { + videoFrameRenderControl.onStreamChanged( + RELEASE_FIRST_FRAME_WHEN_PREVIOUS_STREAM_PROCESSED, startPositionUs); + this.streamStartPositionUs = startPositionUs; + } } @Override 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 3c9baa8ac4..7e9a724821 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 @@ -1643,7 +1643,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer } /** - * Called when ready to {@linkplain VideoSink#onInputStreamChanged(int, Format, List) + * Called when ready to {@linkplain VideoSink#onInputStreamChanged(int, Format, long, List) * change} the input stream. * *

The default implementation applies this renderer's video effects. @@ -1651,7 +1651,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer protected void changeVideoSinkInputStream( VideoSink videoSink, @VideoSink.InputType int inputType, Format format) { List videoEffectsToApply = videoEffects != null ? videoEffects : ImmutableList.of(); - videoSink.onInputStreamChanged(inputType, format, videoEffectsToApply); + videoSink.onInputStreamChanged( + inputType, format, getOutputStreamStartPositionUs(), videoEffectsToApply); } @Override @@ -1861,7 +1862,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer if (videoSink != null) { // Signaling end of the previous stream. videoSink.signalEndOfCurrentInputStream(); - videoSink.setStreamStartPositionUs(getOutputStreamStartPositionUs()); if (this.startPositionUs == C.TIME_UNSET) { this.startPositionUs = getOutputStreamStartPositionUs(); } 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 5c8469ac54..a3cd3acb3e 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 @@ -407,15 +407,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(); - defaultVideoSink.onInputStreamChanged( - INPUT_TYPE_SURFACE, videoGraphOutputFormat, /* videoEffects= */ ImmutableList.of()); + onOutputStreamChanged(); } @Override public void onOutputFrameRateChanged(float frameRate) { videoGraphOutputFormat = videoGraphOutputFormat.buildUpon().setFrameRate(frameRate).build(); - defaultVideoSink.onInputStreamChanged( - INPUT_TYPE_SURFACE, videoGraphOutputFormat, /* videoEffects= */ ImmutableList.of()); + onOutputStreamChanged(); } @Override @@ -436,8 +434,8 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video streamStartPositionsUs.pollFloor(bufferPresentationTimeUs); if (newOutputStreamStartPositionUs != null && newOutputStreamStartPositionUs != outputStreamStartPositionUs) { - defaultVideoSink.setStreamStartPositionUs(newOutputStreamStartPositionUs); outputStreamStartPositionUs = newOutputStreamStartPositionUs; + onOutputStreamChanged(); } boolean isLastFrame = finalBufferPresentationTimeUs != C.TIME_UNSET @@ -580,9 +578,9 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video streamStartPositionsUs.pollFirst(); } if (streamStartPositionsUs.size() == 1) { - long lastStartPositionUs = checkNotNull(streamStartPositionsUs.pollFirst()); - // defaultVideoSink should use the latest startPositionUs if none is passed after flushing. - defaultVideoSink.setStreamStartPositionUs(lastStartPositionUs); + // Use the latest startPositionUs if none is passed after flushing. + outputStreamStartPositionUs = checkNotNull(streamStartPositionsUs.pollFirst()); + onOutputStreamChanged(); } lastOutputBufferPresentationTimeUs = C.TIME_UNSET; finalBufferPresentationTimeUs = C.TIME_UNSET; @@ -619,6 +617,14 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video return inputColorInfo; } + private void onOutputStreamChanged() { + defaultVideoSink.onInputStreamChanged( + INPUT_TYPE_SURFACE, + videoGraphOutputFormat, + outputStreamStartPositionUs, + /* videoEffects= */ ImmutableList.of()); + } + /** Receives input from an ExoPlayer renderer and forwards it to the video graph. */ private final class InputVideoSink implements VideoSink, PlaybackVideoGraphWrapper.Listener { @@ -740,7 +746,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video @Override public void onInputStreamChanged( - @InputType int inputType, Format format, List videoEffects) { + @InputType int inputType, Format format, long startPositionUs, List videoEffects) { checkState(isInitialized()); switch (inputType) { case INPUT_TYPE_SURFACE: @@ -755,6 +761,12 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video finalBufferPresentationTimeUs = C.TIME_UNSET; 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); } @Override @@ -785,16 +797,6 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video } } - @Override - public void setStreamStartPositionUs(long streamStartPositionUs) { - // 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, - streamStartPositionUs); - } - @Override public void setBufferTimestampAdjustmentUs(long bufferTimestampAdjustmentUs) { inputBufferTimestampAdjustmentUs = bufferTimestampAdjustmentUs; 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 6c418cb66a..cc766c60af 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 @@ -188,7 +188,7 @@ public interface VideoSink { * *

This method returns {@code true} if the end of the last input stream has been {@linkplain * #signalEndOfCurrentInputStream() signaled} and all the input frames have been rendered. Note - * that a new input stream can be {@linkplain #onInputStreamChanged(int, Format, List) + * that a new input stream can be {@linkplain #onInputStreamChanged(int, Format, long, List) * signaled} even when this method returns true (in which case the sink will not be ended * anymore). */ @@ -210,14 +210,6 @@ public interface VideoSink { /** Sets {@linkplain Effect video effects} to apply immediately. */ void setVideoEffects(List videoEffects); - /** - * Sets the current stream start position. - * - * @param streamStartPositionUs The start position of the buffer presentation timestamps of the - * current stream, in microseconds. - */ - void setStreamStartPositionUs(long streamStartPositionUs); - /** * Sets the buffer timestamp adjustment. * @@ -256,15 +248,18 @@ public interface VideoSink { * * @param inputType The {@link InputType} of the stream. * @param format The {@link Format} of the stream. + * @param startPositionUs The start position of the buffer presentation timestamps of the stream, + * in microseconds. * @param videoEffects The {@link List} to apply to the new stream. */ - void onInputStreamChanged(@InputType int inputType, Format format, List videoEffects); + void onInputStreamChanged( + @InputType int inputType, Format format, long startPositionUs, List videoEffects); /** * Handles a video input frame. * *

Must be called after the corresponding stream is {@linkplain #onInputStreamChanged(int, - * Format, List) signaled}. + * Format, long, List) signaled}. * * @param framePresentationTimeUs The frame's presentation time, in microseconds. * @param isLastFrame Whether this is the last frame of the video stream. This flag is set on a @@ -281,7 +276,7 @@ public interface VideoSink { * Handles an input {@link Bitmap}. * *

Must be called after the corresponding stream is {@linkplain #onInputStreamChanged(int, - * Format, List) signaled}. + * Format, long, List) signaled}. * * @param inputBitmap The {@link Bitmap} to queue to the video sink. * @param timestampIterator The times within the current stream that the bitmap should be shown diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapperTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapperTest.java index 54ee6a0258..3ed25146ff 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapperTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapperTest.java @@ -75,13 +75,15 @@ public final class PlaybackVideoGraphWrapperTest { PlaybackVideoGraphWrapper playbackVideoGraphWrapper = createPlaybackVideoGraphWrapper(testVideoGraphFactory); Format format = new Format.Builder().build(); + long startPositionUs = 0; VideoSink sink = playbackVideoGraphWrapper.getSink(/* inputIndex= */ 0); sink.initialize(format); - sink.onInputStreamChanged(VideoSink.INPUT_TYPE_SURFACE, format, firstEffects); - sink.onInputStreamChanged(VideoSink.INPUT_TYPE_SURFACE, format, secondEffects); - sink.onInputStreamChanged(VideoSink.INPUT_TYPE_SURFACE, format, ImmutableList.of()); + sink.onInputStreamChanged(VideoSink.INPUT_TYPE_SURFACE, format, startPositionUs, firstEffects); + sink.onInputStreamChanged(VideoSink.INPUT_TYPE_SURFACE, format, startPositionUs, secondEffects); + sink.onInputStreamChanged( + VideoSink.INPUT_TYPE_SURFACE, format, startPositionUs, ImmutableList.of()); testVideoGraphFactory.verifyRegisteredEffectsMatches(/* invocationTimes= */ 3); assertThat(testVideoGraphFactory.getCapturedEffects()) .isEqualTo(ImmutableList.of(firstEffects, secondEffects, ImmutableList.of())); 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 44512f814e..5be9ccfbed 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/BufferingVideoSink.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/BufferingVideoSink.java @@ -192,11 +192,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; executeOrDelay(videoSink -> videoSink.setVideoEffects(videoEffects)); } - @Override - public void setStreamStartPositionUs(long streamStartPositionUs) { - executeOrDelay(videoSink -> videoSink.setStreamStartPositionUs(streamStartPositionUs)); - } - @Override public void setBufferTimestampAdjustmentUs(long bufferTimestampAdjustmentUs) { executeOrDelay( @@ -225,8 +220,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void onInputStreamChanged( - @InputType int inputType, Format format, List videoEffects) { - executeOrDelay(videoSink -> videoSink.onInputStreamChanged(inputType, format, videoEffects)); + @InputType int inputType, Format format, long startPositionUs, List videoEffects) { + executeOrDelay( + videoSink -> + videoSink.onInputStreamChanged(inputType, format, startPositionUs, videoEffects)); } /** 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 80654a6bd4..8e4ede7b17 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java @@ -435,7 +435,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override protected void changeVideoSinkInputStream( VideoSink videoSink, @VideoSink.InputType int inputType, Format format) { - videoSink.onInputStreamChanged(inputType, format, pendingEffects); + videoSink.onInputStreamChanged( + inputType, format, getOutputStreamStartPositionUs(), pendingEffects); } private void activateBufferingVideoSink() { @@ -614,7 +615,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; long positionUs, long elapsedRealtimeUs, Bitmap outputImage, long timeUs) { if (inputStreamPending) { checkState(streamStartPositionUs != C.TIME_UNSET); - videoSink.setStreamStartPositionUs(streamStartPositionUs); videoSink.onInputStreamChanged( VideoSink.INPUT_TYPE_BITMAP, new Format.Builder() @@ -624,6 +624,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; .setColorInfo(ColorInfo.SRGB_BT709_FULL) .setFrameRate(/* frameRate= */ DEFAULT_FRAME_RATE) .build(), + streamStartPositionUs, videoEffects); inputStreamPending = false; }