Add VideoSink.signalEndOfCurrentInputStream()

The isLastBuffer flag passed to MediaCodecRenderer.processOutputBuffer()
is unreliable. It is true for the last frames (plural) of a video
stream, which makes it possible for the video renderer to end before all
the frames have been rendered to the output surface.

PiperOrigin-RevId: 700941685
This commit is contained in:
kimvde 2024-11-28 01:20:48 -08:00 committed by Copybara-Service
parent dfe3c90f9a
commit f3f4646296
6 changed files with 45 additions and 9 deletions

View file

@ -111,6 +111,11 @@ import java.util.concurrent.Executor;
return videoFrameReleaseControl.isReady(rendererOtherwiseReady); return videoFrameReleaseControl.isReady(rendererOtherwiseReady);
} }
@Override
public void signalEndOfCurrentInputStream() {
throw new UnsupportedOperationException();
}
@Override @Override
public boolean isEnded() { public boolean isEnded() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();

View file

@ -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 * Returns the timestamp that is added to the buffer presentation time (the player decoding
* position) to get the frame presentation time, in microseconds. * position) to get the frame presentation time, in microseconds.
@ -1608,6 +1615,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
protected void onProcessedStreamChange() { protected void onProcessedStreamChange() {
super.onProcessedStreamChange(); super.onProcessedStreamChange();
if (videoSink != null) { if (videoSink != null) {
// Signaling end of the previous stream.
videoSink.signalEndOfCurrentInputStream();
videoSink.setStreamTimestampInfo( videoSink.setStreamTimestampInfo(
getOutputStreamStartPositionUs(), getOutputStreamStartPositionUs(),
getBufferTimestampAdjustmentUs(), getBufferTimestampAdjustmentUs(),

View file

@ -605,6 +605,11 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
/* rendererOtherwiseReady= */ rendererOtherwiseReady && isInitialized()); /* rendererOtherwiseReady= */ rendererOtherwiseReady && isInitialized());
} }
@Override
public void signalEndOfCurrentInputStream() {
finalBufferPresentationTimeUs = lastBufferPresentationTimeUs;
}
@Override @Override
public boolean isEnded() { public boolean isEnded() {
return isInitialized() return isInitialized()
@ -742,9 +747,6 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
} }
lastBufferPresentationTimeUs = bufferPresentationTimeUs; lastBufferPresentationTimeUs = bufferPresentationTimeUs;
if (isLastFrame) {
finalBufferPresentationTimeUs = bufferPresentationTimeUs;
}
// Use the frame presentation time as render time so that the SurfaceTexture is accompanied // 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 // 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. // 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; timestampIterator.getLastTimestampUs() - inputBufferTimestampAdjustmentUs;
checkState(lastBufferPresentationTimeUs != C.TIME_UNSET); checkState(lastBufferPresentationTimeUs != C.TIME_UNSET);
this.lastBufferPresentationTimeUs = lastBufferPresentationTimeUs; this.lastBufferPresentationTimeUs = lastBufferPresentationTimeUs;
finalBufferPresentationTimeUs = lastBufferPresentationTimeUs;
return true; return true;
} }

View file

@ -177,7 +177,17 @@ public interface VideoSink {
*/ */
boolean isReady(boolean rendererOtherwiseReady); 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.
*
* <p>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(); boolean isEnded();
/** /**
@ -251,10 +261,12 @@ public interface VideoSink {
* Handles a video input frame. * Handles a video input frame.
* *
* <p>Must be called after the corresponding stream is {@linkplain #onInputStreamChanged(int, * <p>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 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 positionUs The current playback position, in microseconds.
* @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, taken * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, taken
* approximately at the time the playback position was {@code positionUs}. * approximately at the time the playback position was {@code positionUs}.
@ -274,7 +286,7 @@ public interface VideoSink {
* Handles an input {@link Bitmap}. * Handles an input {@link Bitmap}.
* *
* <p>Must be called after the corresponding stream is {@linkplain #onInputStreamChanged(int, * <p>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 inputBitmap The {@link Bitmap} to queue to the video sink.
* @param timestampIterator The times within the current stream that the bitmap should be shown * @param timestampIterator The times within the current stream that the bitmap should be shown

View file

@ -134,6 +134,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return videoSink == null || videoSink.isReady(rendererOtherwiseReady); return videoSink == null || videoSink.isReady(rendererOtherwiseReady);
} }
@Override
public void signalEndOfCurrentInputStream() {
executeOrDelay(VideoSink::signalEndOfCurrentInputStream);
}
@Override @Override
public boolean isEnded() { public boolean isEnded() {
return videoSink != null && videoSink.isEnded(); return videoSink != null && videoSink.isEnded();

View file

@ -475,7 +475,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
.build()); .build());
inputStreamPending = false; inputStreamPending = false;
} }
return videoSink.handleInputBitmap(outputImage, checkStateNotNull(timestampIterator)); if (!videoSink.handleInputBitmap(outputImage, checkStateNotNull(timestampIterator))) {
return false;
}
videoSink.signalEndOfCurrentInputStream();
return true;
} }
@Override @Override