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);
}
@Override
public void signalEndOfCurrentInputStream() {
throw new UnsupportedOperationException();
}
@Override
public boolean isEnded() {
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
* 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(),

View File

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

View File

@ -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.
*
* <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();
/**
@ -251,10 +261,12 @@ public interface VideoSink {
* Handles a video input frame.
*
* <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 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}.
*
* <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 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);
}
@Override
public void signalEndOfCurrentInputStream() {
executeOrDelay(VideoSink::signalEndOfCurrentInputStream);
}
@Override
public boolean isEnded() {
return videoSink != null && videoSink.isEnded();

View File

@ -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