VideoSink: merge setStreamStartPositionUs and onInputStreamChanged

setStreamStartPositionUs and onInputStreamChanged should both be called
when the stream changes.

PiperOrigin-RevId: 736121598
This commit is contained in:
kimvde 2025-03-12 07:15:35 -07:00 committed by Copybara-Service
parent 99767c6e25
commit 03892cc1b5
7 changed files with 49 additions and 56 deletions

View File

@ -177,15 +177,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
throw new UnsupportedOperationException(); 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 @Override
public void setBufferTimestampAdjustmentUs(long bufferTimestampAdjustmentUs) { public void setBufferTimestampAdjustmentUs(long bufferTimestampAdjustmentUs) {
this.bufferTimestampAdjustmentUs = bufferTimestampAdjustmentUs; this.bufferTimestampAdjustmentUs = bufferTimestampAdjustmentUs;
@ -220,7 +211,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/ */
@Override @Override
public void onInputStreamChanged( public void onInputStreamChanged(
@InputType int inputType, Format format, List<Effect> videoEffects) { @InputType int inputType, Format format, long startPositionUs, List<Effect> videoEffects) {
checkState(videoEffects.isEmpty()); checkState(videoEffects.isEmpty());
if (format.width != inputFormat.width || format.height != inputFormat.height) { if (format.width != inputFormat.width || format.height != inputFormat.height) {
videoFrameRenderControl.onVideoSizeChanged(format.width, format.height); videoFrameRenderControl.onVideoSizeChanged(format.width, format.height);
@ -229,6 +220,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
videoFrameReleaseControl.setFrameRate(format.frameRate); videoFrameReleaseControl.setFrameRate(format.frameRate);
} }
inputFormat = format; inputFormat = format;
if (startPositionUs != this.streamStartPositionUs) {
videoFrameRenderControl.onStreamChanged(
RELEASE_FIRST_FRAME_WHEN_PREVIOUS_STREAM_PROCESSED, startPositionUs);
this.streamStartPositionUs = startPositionUs;
}
} }
@Override @Override

View File

@ -1643,7 +1643,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
} }
/** /**
* Called when ready to {@linkplain VideoSink#onInputStreamChanged(int, Format, List<Effect>) * Called when ready to {@linkplain VideoSink#onInputStreamChanged(int, Format, long, List)
* change} the input stream. * change} the input stream.
* *
* <p>The default implementation applies this renderer's video effects. * <p>The default implementation applies this renderer's video effects.
@ -1651,7 +1651,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
protected void changeVideoSinkInputStream( protected void changeVideoSinkInputStream(
VideoSink videoSink, @VideoSink.InputType int inputType, Format format) { VideoSink videoSink, @VideoSink.InputType int inputType, Format format) {
List<Effect> videoEffectsToApply = videoEffects != null ? videoEffects : ImmutableList.of(); List<Effect> videoEffectsToApply = videoEffects != null ? videoEffects : ImmutableList.of();
videoSink.onInputStreamChanged(inputType, format, videoEffectsToApply); videoSink.onInputStreamChanged(
inputType, format, getOutputStreamStartPositionUs(), videoEffectsToApply);
} }
@Override @Override
@ -1861,7 +1862,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
if (videoSink != null) { if (videoSink != null) {
// Signaling end of the previous stream. // Signaling end of the previous stream.
videoSink.signalEndOfCurrentInputStream(); videoSink.signalEndOfCurrentInputStream();
videoSink.setStreamStartPositionUs(getOutputStreamStartPositionUs());
if (this.startPositionUs == C.TIME_UNSET) { if (this.startPositionUs == C.TIME_UNSET) {
this.startPositionUs = getOutputStreamStartPositionUs(); this.startPositionUs = getOutputStreamStartPositionUs();
} }

View File

@ -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. // We forward output size changes to the sink even if we are still flushing.
videoGraphOutputFormat = videoGraphOutputFormat =
videoGraphOutputFormat.buildUpon().setWidth(width).setHeight(height).build(); videoGraphOutputFormat.buildUpon().setWidth(width).setHeight(height).build();
defaultVideoSink.onInputStreamChanged( onOutputStreamChanged();
INPUT_TYPE_SURFACE, videoGraphOutputFormat, /* videoEffects= */ ImmutableList.of());
} }
@Override @Override
public void onOutputFrameRateChanged(float frameRate) { public void onOutputFrameRateChanged(float frameRate) {
videoGraphOutputFormat = videoGraphOutputFormat.buildUpon().setFrameRate(frameRate).build(); videoGraphOutputFormat = videoGraphOutputFormat.buildUpon().setFrameRate(frameRate).build();
defaultVideoSink.onInputStreamChanged( onOutputStreamChanged();
INPUT_TYPE_SURFACE, videoGraphOutputFormat, /* videoEffects= */ ImmutableList.of());
} }
@Override @Override
@ -436,8 +434,8 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
streamStartPositionsUs.pollFloor(bufferPresentationTimeUs); streamStartPositionsUs.pollFloor(bufferPresentationTimeUs);
if (newOutputStreamStartPositionUs != null if (newOutputStreamStartPositionUs != null
&& newOutputStreamStartPositionUs != outputStreamStartPositionUs) { && newOutputStreamStartPositionUs != outputStreamStartPositionUs) {
defaultVideoSink.setStreamStartPositionUs(newOutputStreamStartPositionUs);
outputStreamStartPositionUs = newOutputStreamStartPositionUs; outputStreamStartPositionUs = newOutputStreamStartPositionUs;
onOutputStreamChanged();
} }
boolean isLastFrame = boolean isLastFrame =
finalBufferPresentationTimeUs != C.TIME_UNSET finalBufferPresentationTimeUs != C.TIME_UNSET
@ -580,9 +578,9 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
streamStartPositionsUs.pollFirst(); streamStartPositionsUs.pollFirst();
} }
if (streamStartPositionsUs.size() == 1) { if (streamStartPositionsUs.size() == 1) {
long lastStartPositionUs = checkNotNull(streamStartPositionsUs.pollFirst()); // Use the latest startPositionUs if none is passed after flushing.
// defaultVideoSink should use the latest startPositionUs if none is passed after flushing. outputStreamStartPositionUs = checkNotNull(streamStartPositionsUs.pollFirst());
defaultVideoSink.setStreamStartPositionUs(lastStartPositionUs); onOutputStreamChanged();
} }
lastOutputBufferPresentationTimeUs = C.TIME_UNSET; lastOutputBufferPresentationTimeUs = C.TIME_UNSET;
finalBufferPresentationTimeUs = C.TIME_UNSET; finalBufferPresentationTimeUs = C.TIME_UNSET;
@ -619,6 +617,14 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
return inputColorInfo; 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. */ /** Receives input from an ExoPlayer renderer and forwards it to the video graph. */
private final class InputVideoSink implements VideoSink, PlaybackVideoGraphWrapper.Listener { private final class InputVideoSink implements VideoSink, PlaybackVideoGraphWrapper.Listener {
@ -740,7 +746,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
@Override @Override
public void onInputStreamChanged( public void onInputStreamChanged(
@InputType int inputType, Format format, List<Effect> videoEffects) { @InputType int inputType, Format format, long startPositionUs, List<Effect> videoEffects) {
checkState(isInitialized()); checkState(isInitialized());
switch (inputType) { switch (inputType) {
case INPUT_TYPE_SURFACE: case INPUT_TYPE_SURFACE:
@ -755,6 +761,12 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
finalBufferPresentationTimeUs = C.TIME_UNSET; finalBufferPresentationTimeUs = C.TIME_UNSET;
hasSignaledEndOfCurrentInputStream = false; hasSignaledEndOfCurrentInputStream = false;
registerInputStream(format); 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 @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 @Override
public void setBufferTimestampAdjustmentUs(long bufferTimestampAdjustmentUs) { public void setBufferTimestampAdjustmentUs(long bufferTimestampAdjustmentUs) {
inputBufferTimestampAdjustmentUs = bufferTimestampAdjustmentUs; inputBufferTimestampAdjustmentUs = bufferTimestampAdjustmentUs;

View File

@ -188,7 +188,7 @@ public interface VideoSink {
* *
* <p>This method returns {@code true} if the end of the last input stream has been {@linkplain * <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 * #signalEndOfCurrentInputStream() signaled} and all the input frames have been rendered. Note
* that a new input stream can be {@linkplain #onInputStreamChanged(int, Format, List<Effect>) * 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 * signaled} even when this method returns true (in which case the sink will not be ended
* anymore). * anymore).
*/ */
@ -210,14 +210,6 @@ public interface VideoSink {
/** Sets {@linkplain Effect video effects} to apply immediately. */ /** Sets {@linkplain Effect video effects} to apply immediately. */
void setVideoEffects(List<Effect> videoEffects); void setVideoEffects(List<Effect> 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. * Sets the buffer timestamp adjustment.
* *
@ -256,15 +248,18 @@ public interface VideoSink {
* *
* @param inputType The {@link InputType} of the stream. * @param inputType The {@link InputType} of the stream.
* @param format The {@link Format} 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<Effect>} to apply to the new stream. * @param videoEffects The {@link List<Effect>} to apply to the new stream.
*/ */
void onInputStreamChanged(@InputType int inputType, Format format, List<Effect> videoEffects); void onInputStreamChanged(
@InputType int inputType, Format format, long startPositionUs, List<Effect> videoEffects);
/** /**
* 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, List<Effect>) signaled}. * Format, long, List) 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. This flag is set on a * @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}. * 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, List<Effect>) signaled}. * Format, long, List) 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

@ -75,13 +75,15 @@ public final class PlaybackVideoGraphWrapperTest {
PlaybackVideoGraphWrapper playbackVideoGraphWrapper = PlaybackVideoGraphWrapper playbackVideoGraphWrapper =
createPlaybackVideoGraphWrapper(testVideoGraphFactory); createPlaybackVideoGraphWrapper(testVideoGraphFactory);
Format format = new Format.Builder().build(); Format format = new Format.Builder().build();
long startPositionUs = 0;
VideoSink sink = playbackVideoGraphWrapper.getSink(/* inputIndex= */ 0); VideoSink sink = playbackVideoGraphWrapper.getSink(/* inputIndex= */ 0);
sink.initialize(format); sink.initialize(format);
sink.onInputStreamChanged(VideoSink.INPUT_TYPE_SURFACE, format, firstEffects); sink.onInputStreamChanged(VideoSink.INPUT_TYPE_SURFACE, format, startPositionUs, firstEffects);
sink.onInputStreamChanged(VideoSink.INPUT_TYPE_SURFACE, format, secondEffects); sink.onInputStreamChanged(VideoSink.INPUT_TYPE_SURFACE, format, startPositionUs, secondEffects);
sink.onInputStreamChanged(VideoSink.INPUT_TYPE_SURFACE, format, ImmutableList.of()); sink.onInputStreamChanged(
VideoSink.INPUT_TYPE_SURFACE, format, startPositionUs, ImmutableList.of());
testVideoGraphFactory.verifyRegisteredEffectsMatches(/* invocationTimes= */ 3); testVideoGraphFactory.verifyRegisteredEffectsMatches(/* invocationTimes= */ 3);
assertThat(testVideoGraphFactory.getCapturedEffects()) assertThat(testVideoGraphFactory.getCapturedEffects())
.isEqualTo(ImmutableList.of(firstEffects, secondEffects, ImmutableList.of())); .isEqualTo(ImmutableList.of(firstEffects, secondEffects, ImmutableList.of()));

View File

@ -192,11 +192,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
executeOrDelay(videoSink -> videoSink.setVideoEffects(videoEffects)); executeOrDelay(videoSink -> videoSink.setVideoEffects(videoEffects));
} }
@Override
public void setStreamStartPositionUs(long streamStartPositionUs) {
executeOrDelay(videoSink -> videoSink.setStreamStartPositionUs(streamStartPositionUs));
}
@Override @Override
public void setBufferTimestampAdjustmentUs(long bufferTimestampAdjustmentUs) { public void setBufferTimestampAdjustmentUs(long bufferTimestampAdjustmentUs) {
executeOrDelay( executeOrDelay(
@ -225,8 +220,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public void onInputStreamChanged( public void onInputStreamChanged(
@InputType int inputType, Format format, List<Effect> videoEffects) { @InputType int inputType, Format format, long startPositionUs, List<Effect> videoEffects) {
executeOrDelay(videoSink -> videoSink.onInputStreamChanged(inputType, format, videoEffects)); executeOrDelay(
videoSink ->
videoSink.onInputStreamChanged(inputType, format, startPositionUs, videoEffects));
} }
/** /**

View File

@ -435,7 +435,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
protected void changeVideoSinkInputStream( protected void changeVideoSinkInputStream(
VideoSink videoSink, @VideoSink.InputType int inputType, Format format) { VideoSink videoSink, @VideoSink.InputType int inputType, Format format) {
videoSink.onInputStreamChanged(inputType, format, pendingEffects); videoSink.onInputStreamChanged(
inputType, format, getOutputStreamStartPositionUs(), pendingEffects);
} }
private void activateBufferingVideoSink() { private void activateBufferingVideoSink() {
@ -614,7 +615,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
long positionUs, long elapsedRealtimeUs, Bitmap outputImage, long timeUs) { long positionUs, long elapsedRealtimeUs, Bitmap outputImage, long timeUs) {
if (inputStreamPending) { if (inputStreamPending) {
checkState(streamStartPositionUs != C.TIME_UNSET); checkState(streamStartPositionUs != C.TIME_UNSET);
videoSink.setStreamStartPositionUs(streamStartPositionUs);
videoSink.onInputStreamChanged( videoSink.onInputStreamChanged(
VideoSink.INPUT_TYPE_BITMAP, VideoSink.INPUT_TYPE_BITMAP,
new Format.Builder() new Format.Builder()
@ -624,6 +624,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
.setColorInfo(ColorInfo.SRGB_BT709_FULL) .setColorInfo(ColorInfo.SRGB_BT709_FULL)
.setFrameRate(/* frameRate= */ DEFAULT_FRAME_RATE) .setFrameRate(/* frameRate= */ DEFAULT_FRAME_RATE)
.build(), .build(),
streamStartPositionUs,
videoEffects); videoEffects);
inputStreamPending = false; inputStreamPending = false;
} }