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 64ab313d1e..6696d82d27 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 @@ -111,6 +111,11 @@ import java.util.concurrent.Executor; return videoFrameReleaseControl.isReady(rendererOtherwiseReady); } + @Override + public void signalEndOfCurrentInputStream() { + throw new UnsupportedOperationException(); + } + @Override public boolean isEnded() { throw new UnsupportedOperationException(); 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 87667cc235..e126d171b6 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 @@ -1547,6 +1547,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer } } + @Override + protected void renderToEndOfStream() { + if (videoSink != null) { + videoSink.signalEndOfCurrentInputStream(); + } + } + /** * Returns the timestamp that is added to the buffer presentation time (the player decoding * position) to get the frame presentation time, in microseconds. @@ -1608,6 +1615,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer protected void onProcessedStreamChange() { super.onProcessedStreamChange(); if (videoSink != null) { + // Signaling end of the previous stream. + videoSink.signalEndOfCurrentInputStream(); videoSink.setStreamTimestampInfo( getOutputStreamStartPositionUs(), getBufferTimestampAdjustmentUs(), 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 9a60e4ec11..5eeb688ff4 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 @@ -605,6 +605,11 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video /* rendererOtherwiseReady= */ rendererOtherwiseReady && isInitialized()); } + @Override + public void signalEndOfCurrentInputStream() { + finalBufferPresentationTimeUs = lastBufferPresentationTimeUs; + } + @Override public boolean isEnded() { return isInitialized() @@ -742,9 +747,6 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video } lastBufferPresentationTimeUs = bufferPresentationTimeUs; - if (isLastFrame) { - finalBufferPresentationTimeUs = bufferPresentationTimeUs; - } // Use the frame presentation time as render time so that the SurfaceTexture is accompanied // by this timestamp. Setting a realtime based release time is only relevant when rendering to // a SurfaceView, but we render to a surface in this case. @@ -766,7 +768,6 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video timestampIterator.getLastTimestampUs() - inputBufferTimestampAdjustmentUs; checkState(lastBufferPresentationTimeUs != C.TIME_UNSET); this.lastBufferPresentationTimeUs = lastBufferPresentationTimeUs; - finalBufferPresentationTimeUs = lastBufferPresentationTimeUs; return true; } 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 4069c38b23..0977a28b1c 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 @@ -177,7 +177,17 @@ public interface VideoSink { */ boolean isReady(boolean rendererOtherwiseReady); - /** Returns whether all the data has been rendered to the output surface. */ + /** Signals the end of the current input stream. */ + void signalEndOfCurrentInputStream(); + + /** + * Returns whether all the data has been rendered to the output surface. + * + *

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) signaled} even + * when this method returns true (in which case the sink will not be ended anymore). + */ boolean isEnded(); /** @@ -251,10 +261,12 @@ public interface VideoSink { * Handles a video input frame. * *

Must be called after the corresponding stream is {@linkplain #onInputStreamChanged(int, - * Format) signalled}. + * Format) signaled}. * * @param framePresentationTimeUs The frame's presentation time, in microseconds. - * @param isLastFrame Whether this is the last frame of the video stream. + * @param isLastFrame Whether this is the last frame of the video stream. This flag is set on a + * best effort basis, and any logic relying on it should degrade gracefully to handle cases + * where it's not set. * @param positionUs The current playback position, in microseconds. * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, taken * approximately at the time the playback position was {@code positionUs}. @@ -274,7 +286,7 @@ public interface VideoSink { * Handles an input {@link Bitmap}. * *

Must be called after the corresponding stream is {@linkplain #onInputStreamChanged(int, - * Format) signalled}. + * Format) 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/transformer/src/main/java/androidx/media3/transformer/BufferingVideoSink.java b/libraries/transformer/src/main/java/androidx/media3/transformer/BufferingVideoSink.java index 7df14902b2..351daab4ad 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/BufferingVideoSink.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/BufferingVideoSink.java @@ -134,6 +134,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return videoSink == null || videoSink.isReady(rendererOtherwiseReady); } + @Override + public void signalEndOfCurrentInputStream() { + executeOrDelay(VideoSink::signalEndOfCurrentInputStream); + } + @Override public boolean isEnded() { return videoSink != null && videoSink.isEnded(); 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 615e87ff5f..c5aa7df7d2 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java @@ -475,7 +475,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; .build()); inputStreamPending = false; } - return videoSink.handleInputBitmap(outputImage, checkStateNotNull(timestampIterator)); + if (!videoSink.handleInputBitmap(outputImage, checkStateNotNull(timestampIterator))) { + return false; + } + videoSink.signalEndOfCurrentInputStream(); + return true; } @Override