Implement DefaultVideoSink.isEnded

To do that, I had to add VideoFrameRenderControl.signalEndOfInput() and
isEnded() to match the VideoSink interface.

PiperOrigin-RevId: 700957098
This commit is contained in:
kimvde 2024-11-28 02:25:30 -08:00 committed by Copybara-Service
parent f3f4646296
commit 852091f2d9
4 changed files with 71 additions and 65 deletions

View File

@ -113,12 +113,12 @@ import java.util.concurrent.Executor;
@Override @Override
public void signalEndOfCurrentInputStream() { public void signalEndOfCurrentInputStream() {
throw new UnsupportedOperationException(); videoFrameRenderControl.signalEndOfInput();
} }
@Override @Override
public boolean isEnded() { public boolean isEnded() {
throw new UnsupportedOperationException(); return videoFrameRenderControl.isEnded();
} }
@Override @Override

View File

@ -252,6 +252,11 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
private @State int state; private @State int state;
@Nullable private Renderer.WakeupListener wakeupListener; @Nullable private Renderer.WakeupListener wakeupListener;
/** The buffer presentation time, in microseconds, of the final frame in the stream. */
private long finalBufferPresentationTimeUs;
private boolean hasSignaledEndOfCurrentInputStream;
/** /**
* Converts the buffer timestamp (the player position, with renderer offset) to the composition * Converts the buffer timestamp (the player position, with renderer offset) to the composition
* timestamp, in microseconds. The composition time starts from zero, add this adjustment to * timestamp, in microseconds. The composition time starts from zero, add this adjustment to
@ -275,6 +280,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
state = STATE_CREATED; state = STATE_CREATED;
videoGraphOutputFormat = new Format.Builder().build(); videoGraphOutputFormat = new Format.Builder().build();
addListener(inputVideoSink); addListener(inputVideoSink);
finalBufferPresentationTimeUs = C.TIME_UNSET;
} }
/** /**
@ -378,6 +384,12 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
outputStreamStartPositionUs = newOutputStreamStartPositionUs; outputStreamStartPositionUs = newOutputStreamStartPositionUs;
} }
videoFrameRenderControl.onFrameAvailableForRendering(bufferPresentationTimeUs); videoFrameRenderControl.onFrameAvailableForRendering(bufferPresentationTimeUs);
if (finalBufferPresentationTimeUs != C.TIME_UNSET
&& bufferPresentationTimeUs >= finalBufferPresentationTimeUs) {
// TODO b/257464707 - Support extensively modified media.
defaultVideoSink.signalEndOfCurrentInputStream();
hasSignaledEndOfCurrentInputStream = true;
}
} }
@Override @Override
@ -454,8 +466,10 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
/* rendererOtherwiseReady= */ rendererOtherwiseReady && pendingFlushCount == 0); /* rendererOtherwiseReady= */ rendererOtherwiseReady && pendingFlushCount == 0);
} }
private boolean hasReleasedFrame(long presentationTimeUs) { private boolean isEnded() {
return pendingFlushCount == 0 && videoFrameRenderControl.hasReleasedFrame(presentationTimeUs); return pendingFlushCount == 0
&& hasSignaledEndOfCurrentInputStream
&& defaultVideoSink.isEnded();
} }
/** /**
@ -484,6 +498,8 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
defaultVideoSink.setStreamTimestampInfo( defaultVideoSink.setStreamTimestampInfo(
lastStartPositionUs, /* unused */ C.TIME_UNSET, /* unused */ C.TIME_UNSET); lastStartPositionUs, /* unused */ C.TIME_UNSET, /* unused */ C.TIME_UNSET);
} }
finalBufferPresentationTimeUs = C.TIME_UNSET;
hasSignaledEndOfCurrentInputStream = false;
// Handle pending video graph callbacks to ensure video size changes reach the video render // Handle pending video graph callbacks to ensure video size changes reach the video render
// control. // control.
checkStateNotNull(handler).post(() -> pendingFlushCount--); checkStateNotNull(handler).post(() -> pendingFlushCount--);
@ -522,9 +538,6 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
private long inputBufferTimestampAdjustmentUs; private long inputBufferTimestampAdjustmentUs;
private long lastResetPositionUs; private long lastResetPositionUs;
/** The buffer presentation time, in microseconds, of the final frame in the stream. */
private long finalBufferPresentationTimeUs;
/** /**
* The buffer presentation timestamp, in microseconds, of the most recently registered frame. * The buffer presentation timestamp, in microseconds, of the most recently registered frame.
*/ */
@ -541,7 +554,6 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
videoFrameProcessorMaxPendingFrameCount = videoFrameProcessorMaxPendingFrameCount =
Util.getMaxPendingFramesCountForMediaCodecDecoders(context); Util.getMaxPendingFramesCountForMediaCodecDecoders(context);
videoEffects = ImmutableList.of(); videoEffects = ImmutableList.of();
finalBufferPresentationTimeUs = C.TIME_UNSET;
lastBufferPresentationTimeUs = C.TIME_UNSET; lastBufferPresentationTimeUs = C.TIME_UNSET;
listener = VideoSink.Listener.NO_OP; listener = VideoSink.Listener.NO_OP;
listenerExecutor = NO_OP_EXECUTOR; listenerExecutor = NO_OP_EXECUTOR;
@ -590,7 +602,6 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
if (isInitialized()) { if (isInitialized()) {
videoFrameProcessor.flush(); videoFrameProcessor.flush();
} }
finalBufferPresentationTimeUs = C.TIME_UNSET;
lastBufferPresentationTimeUs = C.TIME_UNSET; lastBufferPresentationTimeUs = C.TIME_UNSET;
PlaybackVideoGraphWrapper.this.flush(resetPosition); PlaybackVideoGraphWrapper.this.flush(resetPosition);
// Don't change input stream start position or reset the pending input stream timestamp info // Don't change input stream start position or reset the pending input stream timestamp info
@ -612,9 +623,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
@Override @Override
public boolean isEnded() { public boolean isEnded() {
return isInitialized() return isInitialized() && PlaybackVideoGraphWrapper.this.isEnded();
&& finalBufferPresentationTimeUs != C.TIME_UNSET
&& PlaybackVideoGraphWrapper.this.hasReleasedFrame(finalBufferPresentationTimeUs);
} }
@Override @Override
@ -630,6 +639,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
this.inputType = inputType; this.inputType = inputType;
this.inputFormat = format; this.inputFormat = format;
finalBufferPresentationTimeUs = C.TIME_UNSET; finalBufferPresentationTimeUs = C.TIME_UNSET;
hasSignaledEndOfCurrentInputStream = false;
registerInputStream(format); registerInputStream(format);
} }

View File

@ -77,8 +77,12 @@ import androidx.media3.exoplayer.ExoPlaybackException;
/** A queue of unprocessed input frame timestamps. */ /** A queue of unprocessed input frame timestamps. */
private final LongArrayQueue presentationTimestampsUs; private final LongArrayQueue presentationTimestampsUs;
private long lastInputPresentationTimeUs; private long latestInputPresentationTimeUs;
private long lastOutputPresentationTimeUs; private long latestOutputPresentationTimeUs;
/** The presentation time of the final frame to render. */
private long lastPresentationTimeUs;
private VideoSize outputVideoSize; private VideoSize outputVideoSize;
private long outputStreamStartPositionUs; private long outputStreamStartPositionUs;
@ -91,16 +95,18 @@ import androidx.media3.exoplayer.ExoPlaybackException;
videoSizes = new TimedValueQueue<>(); videoSizes = new TimedValueQueue<>();
streamStartPositionsUs = new TimedValueQueue<>(); streamStartPositionsUs = new TimedValueQueue<>();
presentationTimestampsUs = new LongArrayQueue(); presentationTimestampsUs = new LongArrayQueue();
lastInputPresentationTimeUs = C.TIME_UNSET; latestInputPresentationTimeUs = C.TIME_UNSET;
outputVideoSize = VideoSize.UNKNOWN; outputVideoSize = VideoSize.UNKNOWN;
lastOutputPresentationTimeUs = C.TIME_UNSET; latestOutputPresentationTimeUs = C.TIME_UNSET;
lastPresentationTimeUs = C.TIME_UNSET;
} }
/** Flushes the renderer. */ /** Flushes the renderer. */
public void flush() { public void flush() {
presentationTimestampsUs.clear(); presentationTimestampsUs.clear();
lastInputPresentationTimeUs = C.TIME_UNSET; latestInputPresentationTimeUs = C.TIME_UNSET;
lastOutputPresentationTimeUs = C.TIME_UNSET; latestOutputPresentationTimeUs = C.TIME_UNSET;
lastPresentationTimeUs = C.TIME_UNSET;
if (streamStartPositionsUs.size() > 0) { if (streamStartPositionsUs.size() > 0) {
// There is a pending streaming start position change. If seeking within the same stream, keep // There is a pending streaming start position change. If seeking within the same stream, keep
// the pending start position with min timestamp to ensure the start position is applied on // the pending start position with min timestamp to ensure the start position is applied on
@ -120,18 +126,6 @@ import androidx.media3.exoplayer.ExoPlaybackException;
} }
} }
/**
* Returns whether the renderer has released a frame after a specific presentation timestamp.
*
* @param presentationTimeUs The requested timestamp, in microseconds.
* @return Whether the renderer has released a frame with a timestamp greater than or equal to
* {@code presentationTimeUs}.
*/
public boolean hasReleasedFrame(long presentationTimeUs) {
return lastOutputPresentationTimeUs != C.TIME_UNSET
&& lastOutputPresentationTimeUs >= presentationTimeUs;
}
/** /**
* Incrementally renders available video frames. * Incrementally renders available video frames.
* *
@ -160,17 +154,17 @@ import androidx.media3.exoplayer.ExoPlaybackException;
return; return;
case VideoFrameReleaseControl.FRAME_RELEASE_SKIP: case VideoFrameReleaseControl.FRAME_RELEASE_SKIP:
case VideoFrameReleaseControl.FRAME_RELEASE_DROP: case VideoFrameReleaseControl.FRAME_RELEASE_DROP:
lastOutputPresentationTimeUs = presentationTimeUs; latestOutputPresentationTimeUs = presentationTimeUs;
dropFrame(); dropFrame();
break; break;
case VideoFrameReleaseControl.FRAME_RELEASE_IGNORE: case VideoFrameReleaseControl.FRAME_RELEASE_IGNORE:
// TODO b/293873191 - Handle very late buffers and drop to key frame. Need to flush // TODO b/293873191 - Handle very late buffers and drop to key frame. Need to flush
// VideoGraph input frames in this case. // VideoGraph input frames in this case.
lastOutputPresentationTimeUs = presentationTimeUs; latestOutputPresentationTimeUs = presentationTimeUs;
break; break;
case VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY: case VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY:
case VideoFrameReleaseControl.FRAME_RELEASE_SCHEDULED: case VideoFrameReleaseControl.FRAME_RELEASE_SCHEDULED:
lastOutputPresentationTimeUs = presentationTimeUs; latestOutputPresentationTimeUs = presentationTimeUs;
renderFrame( renderFrame(
/* shouldRenderImmediately= */ frameReleaseAction /* shouldRenderImmediately= */ frameReleaseAction
== VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY); == VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
@ -184,13 +178,13 @@ import androidx.media3.exoplayer.ExoPlaybackException;
/** Called when the size of the available frames has changed. */ /** Called when the size of the available frames has changed. */
public void onVideoSizeChanged(int width, int height) { public void onVideoSizeChanged(int width, int height) {
videoSizes.add( videoSizes.add(
lastInputPresentationTimeUs == C.TIME_UNSET ? 0 : lastInputPresentationTimeUs + 1, latestInputPresentationTimeUs == C.TIME_UNSET ? 0 : latestInputPresentationTimeUs + 1,
new VideoSize(width, height)); new VideoSize(width, height));
} }
public void onStreamStartPositionChanged(long streamStartPositionUs) { public void onStreamStartPositionChanged(long streamStartPositionUs) {
streamStartPositionsUs.add( streamStartPositionsUs.add(
lastInputPresentationTimeUs == C.TIME_UNSET ? 0 : lastInputPresentationTimeUs + 1, latestInputPresentationTimeUs == C.TIME_UNSET ? 0 : latestInputPresentationTimeUs + 1,
streamStartPositionUs); streamStartPositionUs);
} }
@ -201,8 +195,31 @@ import androidx.media3.exoplayer.ExoPlaybackException;
*/ */
public void onFrameAvailableForRendering(long presentationTimeUs) { public void onFrameAvailableForRendering(long presentationTimeUs) {
presentationTimestampsUs.add(presentationTimeUs); presentationTimestampsUs.add(presentationTimeUs);
lastInputPresentationTimeUs = presentationTimeUs; latestInputPresentationTimeUs = presentationTimeUs;
// TODO b/257464707 - Support extensively modified media. lastPresentationTimeUs = C.TIME_UNSET;
}
/**
* Signals the end of input.
*
* <p>If a frame becomes {@linkplain #onFrameAvailableForRendering(long) available} after calling
* this method, the end of input signal is ignored.
*/
public void signalEndOfInput() {
lastPresentationTimeUs = latestInputPresentationTimeUs;
}
/**
* Returns whether all the frames have been rendered to the output surface.
*
* <p>This method returns {@code true} if the last frame that became {@linkplain
* #onFrameAvailableForRendering(long) available} before {@linkplain #signalEndOfInput()
* signalling the end of input} has been rendered, and if no frame has become available in the
* mean time.
*/
public boolean isEnded() {
return lastPresentationTimeUs != C.TIME_UNSET
&& latestOutputPresentationTimeUs == lastPresentationTimeUs;
} }
private void dropFrame() { private void dropFrame() {

View File

@ -230,17 +230,17 @@ public class VideoFrameRenderControlTest {
} }
@Test @Test
public void hasReleasedFrame_noFrameReleased_returnsFalse() { public void isEnded_endOfInputNotSignaled_returnsFalse() {
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl(); VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
VideoFrameRenderControl videoFrameRenderControl = VideoFrameRenderControl videoFrameRenderControl =
new VideoFrameRenderControl( new VideoFrameRenderControl(
mock(VideoFrameRenderControl.FrameRenderer.class), videoFrameReleaseControl); mock(VideoFrameRenderControl.FrameRenderer.class), videoFrameReleaseControl);
assertThat(videoFrameRenderControl.hasReleasedFrame(/* presentationTimeUs= */ 0)).isFalse(); assertThat(videoFrameRenderControl.isEnded()).isFalse();
} }
@Test @Test
public void hasReleasedFrame_frameIsReleased_returnsTrue() throws Exception { public void isEnded_endOfInputSignaled_returnsTrue() throws Exception {
VideoFrameRenderControl.FrameRenderer frameRenderer = VideoFrameRenderControl.FrameRenderer frameRenderer =
mock(VideoFrameRenderControl.FrameRenderer.class); mock(VideoFrameRenderControl.FrameRenderer.class);
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl(); VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
@ -252,22 +252,13 @@ public class VideoFrameRenderControlTest {
/* width= */ VIDEO_WIDTH, /* height= */ VIDEO_HEIGHT); /* width= */ VIDEO_WIDTH, /* height= */ VIDEO_HEIGHT);
videoFrameRenderControl.onFrameAvailableForRendering(/* presentationTimeUs= */ 0); videoFrameRenderControl.onFrameAvailableForRendering(/* presentationTimeUs= */ 0);
videoFrameRenderControl.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); videoFrameRenderControl.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
videoFrameRenderControl.signalEndOfInput();
InOrder inOrder = Mockito.inOrder(frameRenderer); assertThat(videoFrameRenderControl.isEnded()).isTrue();
inOrder
.verify(frameRenderer)
.onVideoSizeChanged(new VideoSize(/* width= */ VIDEO_WIDTH, /* height= */ VIDEO_HEIGHT));
inOrder
.verify(frameRenderer)
.renderFrame(
/* renderTimeNs= */ anyLong(),
/* presentationTimeUs= */ eq(0L),
/* isFirstFrame= */ eq(true));
assertThat(videoFrameRenderControl.hasReleasedFrame(/* presentationTimeUs= */ 0)).isTrue();
} }
@Test @Test
public void hasReleasedFrame_frameIsReleasedAndFlushed_returnsFalse() throws Exception { public void isEnded_afterFlush_returnsFalse() throws Exception {
VideoFrameRenderControl.FrameRenderer frameRenderer = VideoFrameRenderControl.FrameRenderer frameRenderer =
mock(VideoFrameRenderControl.FrameRenderer.class); mock(VideoFrameRenderControl.FrameRenderer.class);
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl(); VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
@ -279,21 +270,9 @@ public class VideoFrameRenderControlTest {
/* width= */ VIDEO_WIDTH, /* height= */ VIDEO_HEIGHT); /* width= */ VIDEO_WIDTH, /* height= */ VIDEO_HEIGHT);
videoFrameRenderControl.onFrameAvailableForRendering(/* presentationTimeUs= */ 0); videoFrameRenderControl.onFrameAvailableForRendering(/* presentationTimeUs= */ 0);
videoFrameRenderControl.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); videoFrameRenderControl.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
InOrder inOrder = Mockito.inOrder(frameRenderer);
inOrder
.verify(frameRenderer)
.onVideoSizeChanged(new VideoSize(/* width= */ VIDEO_WIDTH, /* height= */ VIDEO_HEIGHT));
inOrder
.verify(frameRenderer)
.renderFrame(
/* renderTimeNs= */ anyLong(),
/* presentationTimeUs= */ eq(0L),
/* isFirstFrame= */ eq(true));
videoFrameRenderControl.flush(); videoFrameRenderControl.flush();
assertThat(videoFrameRenderControl.hasReleasedFrame(/* presentationTimeUs= */ 0)).isFalse(); assertThat(videoFrameRenderControl.isEnded()).isFalse();
} }
private static VideoFrameReleaseControl createVideoFrameReleaseControl() { private static VideoFrameReleaseControl createVideoFrameReleaseControl() {