Compare commits

...

3 Commits

Author SHA1 Message Date
kimvde
4d7046c187 Fix failing test
This was introduced by 0f08c97221.

PiperOrigin-RevId: 752273544
2025-04-28 05:52:48 -07:00
kimvde
0f08c97221 Handle rendering in VideoGraph time
Before this CL, the buffer adjustment (which allows to convert from
ExoPlayer time to VideoGraph time) was added to the frame timestamps
before feeding them to the VideoGraph, and then subtracted at the
VideoGraph output. The playback position and stream start position used
for rendering were in ExoPlayer time.

This doesn't work for multi-sequence though because the adjustment might
be different depending on the player (after a seek for example).

To solve this problem, this CL handles rendering in VideoGraph time
instead of ExoPlayer time. More concretely, the VideoGraph output
timestamps are unchanged, and the playback position and stream start
position are converted to VideoGraph time.

PiperOrigin-RevId: 752260744
2025-04-28 04:59:13 -07:00
kimvde
8968d9fa45 Remove VideoSinkProvider
PiperOrigin-RevId: 752227606
2025-04-28 02:48:06 -07:00
10 changed files with 156 additions and 182 deletions

View File

@ -125,6 +125,10 @@
`Player.seekToNextMediaItem()` instead. `Player.seekToNextMediaItem()` instead.
* Removed deprecated `BaseAudioProcessor` in `exoplayer` module. Use * Removed deprecated `BaseAudioProcessor` in `exoplayer` module. Use
`BaseAudioProcessor` under `common` module. `BaseAudioProcessor` under `common` module.
* Remove deprecated `MediaCodecVideoRenderer` constructor
`MediaCodecVideoRenderer(Context, MediaCodecAdapter.Factor,
MediaCodecSelector, long, boolean, @Nullable Handler, @Nullable
VideoRendererEventListener, int, float, @Nullable VideoSinkProvider)`.
## 1.6 ## 1.6

View File

@ -45,6 +45,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* <ul> * <ul>
* <li>Applying video effects * <li>Applying video effects
* <li>Inputting bitmaps * <li>Inputting bitmaps
* <li>Redrawing
* <li>Setting a buffer timestamp adjustment
* </ul> * </ul>
* *
* <p>The {@linkplain #getInputSurface() input} and {@linkplain #setOutputSurfaceInfo(Surface, Size) * <p>The {@linkplain #getInputSurface() input} and {@linkplain #setOutputSurfaceInfo(Surface, Size)
@ -59,7 +61,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable private Surface outputSurface; @Nullable private Surface outputSurface;
private Format inputFormat; private Format inputFormat;
private long streamStartPositionUs; private long streamStartPositionUs;
private long bufferTimestampAdjustmentUs;
private Listener listener; private Listener listener;
private Executor listenerExecutor; private Executor listenerExecutor;
private VideoFrameMetadataListener videoFrameMetadataListener; private VideoFrameMetadataListener videoFrameMetadataListener;
@ -104,6 +105,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return true; return true;
} }
/**
* {@inheritDoc}
*
* <p>This method will always throw an {@link UnsupportedOperationException}.
*/
@Override @Override
public void redraw() { public void redraw() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
@ -163,9 +169,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
/**
* {@inheritDoc}
*
* <p>This method will always throw an {@link UnsupportedOperationException}.
*/
@Override @Override
public void setBufferTimestampAdjustmentUs(long bufferTimestampAdjustmentUs) { public void setBufferTimestampAdjustmentUs(long bufferTimestampAdjustmentUs) {
this.bufferTimestampAdjustmentUs = bufferTimestampAdjustmentUs; throw new UnsupportedOperationException();
} }
@Override @Override
@ -220,8 +231,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public boolean handleInputFrame( public boolean handleInputFrame(
long framePresentationTimeUs, VideoFrameHandler videoFrameHandler) { long framePresentationTimeUs, VideoFrameHandler videoFrameHandler) {
videoFrameHandlers.add(videoFrameHandler); videoFrameHandlers.add(videoFrameHandler);
long bufferPresentationTimeUs = framePresentationTimeUs - bufferTimestampAdjustmentUs; videoFrameRenderControl.onFrameAvailableForRendering(framePresentationTimeUs);
videoFrameRenderControl.onFrameAvailableForRendering(bufferPresentationTimeUs);
listenerExecutor.execute(() -> listener.onFrameAvailableForRendering()); listenerExecutor.execute(() -> listener.onFrameAvailableForRendering());
return true; return true;
} }
@ -232,7 +242,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* <p>This method will always throw an {@link UnsupportedOperationException}. * <p>This method will always throw an {@link UnsupportedOperationException}.
*/ */
@Override @Override
public boolean handleInputBitmap(Bitmap inputBitmap, TimestampIterator timestampIterator) { public boolean handleInputBitmap(Bitmap inputBitmap, TimestampIterator bufferTimestampIterator) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@ -269,8 +279,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
@Override @Override
public void renderFrame( public void renderFrame(long renderTimeNs, long framePresentationTimeUs, boolean isFirstFrame) {
long renderTimeNs, long bufferPresentationTimeUs, boolean isFirstFrame) {
if (isFirstFrame && outputSurface != null) { if (isFirstFrame && outputSurface != null) {
listenerExecutor.execute(() -> listener.onFirstFrameRendered()); listenerExecutor.execute(() -> listener.onFirstFrameRendered());
} }
@ -278,7 +287,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// onVideoSizeChanged is announced after the first frame is available for rendering. // onVideoSizeChanged is announced after the first frame is available for rendering.
Format format = outputFormat == null ? new Format.Builder().build() : outputFormat; Format format = outputFormat == null ? new Format.Builder().build() : outputFormat;
videoFrameMetadataListener.onVideoFrameAboutToBeRendered( videoFrameMetadataListener.onVideoFrameAboutToBeRendered(
/* presentationTimeUs= */ bufferPresentationTimeUs, /* presentationTimeUs= */ framePresentationTimeUs,
/* releaseTimeNs= */ renderTimeNs, /* releaseTimeNs= */ renderTimeNs,
format, format,
/* mediaFormat= */ null); /* mediaFormat= */ null);

View File

@ -537,35 +537,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
.setAssumedMinimumCodecOperatingRate(assumedMinimumCodecOperatingRate)); .setAssumedMinimumCodecOperatingRate(assumedMinimumCodecOperatingRate));
} }
/**
* @deprecated Use {@link Builder} instead.
*/
@Deprecated
public MediaCodecVideoRenderer(
Context context,
MediaCodecAdapter.Factory codecAdapterFactory,
MediaCodecSelector mediaCodecSelector,
long allowedJoiningTimeMs,
boolean enableDecoderFallback,
@Nullable Handler eventHandler,
@Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify,
float assumedMinimumCodecOperatingRate,
@Nullable VideoSinkProvider videoSinkProvider) {
this(
new Builder(context)
.setMediaCodecSelector(mediaCodecSelector)
.setCodecAdapterFactory(codecAdapterFactory)
.setAllowedJoiningTimeMs(allowedJoiningTimeMs)
.setEnableDecoderFallback(enableDecoderFallback)
.setEventHandler(eventHandler)
.setEventListener(eventListener)
.setMaxDroppedFramesToNotify(maxDroppedFramesToNotify)
.setAssumedMinimumCodecOperatingRate(assumedMinimumCodecOperatingRate)
.setVideoSink(
videoSinkProvider == null ? null : videoSinkProvider.getSink(/* inputIndex= */ 0)));
}
/** /**
* @deprecated Use {@link Builder} instead. * @deprecated Use {@link Builder} instead.
*/ */
@ -655,6 +626,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
boolean isLastFrame, boolean isLastFrame,
boolean treatDroppedBuffersAsSkipped) boolean treatDroppedBuffersAsSkipped)
throws ExoPlaybackException { throws ExoPlaybackException {
if (videoSink != null && ownsVideoSink) {
// When using PlaybackVideoGraphWrapper, positionUs is shifted by the buffer timestamp
// adjustment. Shift it back to the player position.
positionUs -= getBufferTimestampAdjustmentUs();
}
if (minEarlyUsToDropDecoderInput != C.TIME_UNSET) { if (minEarlyUsToDropDecoderInput != C.TIME_UNSET) {
// TODO: b/161996553 - Remove the isAwayFromLastResetPosition check when audio pre-rolling // TODO: b/161996553 - Remove the isAwayFromLastResetPosition check when audio pre-rolling
// is implemented correctly. Audio codecs such as Opus require pre-roll samples to be decoded // is implemented correctly. Audio codecs such as Opus require pre-roll samples to be decoded
@ -1763,9 +1739,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
skipOutputBuffer(codec, bufferIndex, presentationTimeUs); skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
return true; return true;
} }
long framePresentationTimeUs = bufferPresentationTimeUs + getBufferTimestampAdjustmentUs();
return videoSink.handleInputFrame( return videoSink.handleInputFrame(
framePresentationTimeUs, bufferPresentationTimeUs,
new VideoSink.VideoFrameHandler() { new VideoSink.VideoFrameHandler() {
@Override @Override
public void render(long renderTimestampNs) { public void render(long renderTimestampNs) {

View File

@ -76,7 +76,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/ */
@UnstableApi @UnstableApi
@RestrictTo({Scope.LIBRARY_GROUP}) @RestrictTo({Scope.LIBRARY_GROUP})
public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, VideoGraph.Listener { public final class PlaybackVideoGraphWrapper implements VideoGraph.Listener {
/** Listener for {@link PlaybackVideoGraphWrapper} events. */ /** Listener for {@link PlaybackVideoGraphWrapper} events. */
public interface Listener { public interface Listener {
@ -326,23 +326,16 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
private @State int state; private @State int state;
/** /**
* The buffer presentation time of the frame most recently output by the video graph, in * The frame presentation time of the frame most recently output by the video graph, in
* microseconds. * microseconds.
*/ */
private long lastOutputBufferPresentationTimeUs; private long lastOutputFramePresentationTimeUs;
/** The buffer presentation time, in microseconds, of the final frame in the stream. */ /** The frame presentation time, in microseconds, of the final frame in the stream. */
private long finalBufferPresentationTimeUs; private long finalFramePresentationTimeUs;
private boolean hasSignaledEndOfVideoGraphOutputStream; private boolean hasSignaledEndOfVideoGraphOutputStream;
/**
* 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
* buffer timestamp to get the composition time.
*/
private long bufferTimestampAdjustmentUs;
private int totalVideoInputCount; private int totalVideoInputCount;
private int registeredVideoInputCount; private int registeredVideoInputCount;
@ -372,8 +365,8 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
requestOpenGlToneMapping = builder.requestOpenGlToneMapping; requestOpenGlToneMapping = builder.requestOpenGlToneMapping;
videoGraphOutputFormat = new Format.Builder().build(); videoGraphOutputFormat = new Format.Builder().build();
outputStreamStartPositionUs = C.TIME_UNSET; outputStreamStartPositionUs = C.TIME_UNSET;
lastOutputBufferPresentationTimeUs = C.TIME_UNSET; lastOutputFramePresentationTimeUs = C.TIME_UNSET;
finalBufferPresentationTimeUs = C.TIME_UNSET; finalFramePresentationTimeUs = C.TIME_UNSET;
totalVideoInputCount = C.LENGTH_UNSET; totalVideoInputCount = C.LENGTH_UNSET;
state = STATE_CREATED; state = STATE_CREATED;
} }
@ -400,19 +393,12 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
this.totalVideoInputCount = totalVideoInputCount; this.totalVideoInputCount = totalVideoInputCount;
} }
/** Starts rendering to the output surface. */ /**
public void startRendering() { * Returns the {@link VideoSink} to forward video frames for processing.
defaultVideoSink.startRendering(); *
} * @param inputIndex The index of the {@link VideoSink}.
* @return The {@link VideoSink} at the given index.
/** Stops rendering to the output surface. */ */
public void stopRendering() {
defaultVideoSink.stopRendering();
}
// VideoSinkProvider methods
@Override
public VideoSink getSink(int inputIndex) { public VideoSink getSink(int inputIndex) {
if (contains(inputVideoSinks, inputIndex)) { if (contains(inputVideoSinks, inputIndex)) {
return inputVideoSinks.get(inputIndex); return inputVideoSinks.get(inputIndex);
@ -425,7 +411,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
return inputVideoSink; return inputVideoSink;
} }
@Override /** Sets the output surface info. */
public void setOutputSurfaceInfo(Surface outputSurface, Size outputResolution) { public void setOutputSurfaceInfo(Surface outputSurface, Size outputResolution) {
if (currentSurfaceAndSize != null if (currentSurfaceAndSize != null
&& currentSurfaceAndSize.first.equals(outputSurface) && currentSurfaceAndSize.first.equals(outputSurface)
@ -437,7 +423,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
outputSurface, outputResolution.getWidth(), outputResolution.getHeight()); outputSurface, outputResolution.getWidth(), outputResolution.getHeight());
} }
@Override /** Clears the set output surface info. */
public void clearOutputSurfaceInfo() { public void clearOutputSurfaceInfo() {
maybeSetOutputSurfaceInfo( maybeSetOutputSurfaceInfo(
/* surface= */ null, /* surface= */ null,
@ -446,7 +432,17 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
currentSurfaceAndSize = null; currentSurfaceAndSize = null;
} }
@Override /** Starts rendering to the output surface. */
public void startRendering() {
defaultVideoSink.startRendering();
}
/** Stops rendering to the output surface. */
public void stopRendering() {
defaultVideoSink.stopRendering();
}
/** Releases the sink provider. */
public void release() { public void release() {
if (state == STATE_RELEASED) { if (state == STATE_RELEASED) {
return; return;
@ -489,12 +485,11 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
listener.onFrameAvailableForRendering(); listener.onFrameAvailableForRendering();
} }
long bufferPresentationTimeUs = framePresentationTimeUs - bufferTimestampAdjustmentUs;
if (isRedrawnFrame) { if (isRedrawnFrame) {
// Redrawn frames are rendered directly in the processing pipeline. // Redrawn frames are rendered directly in the processing pipeline.
if (videoFrameMetadataListener != null) { if (videoFrameMetadataListener != null) {
videoFrameMetadataListener.onVideoFrameAboutToBeRendered( videoFrameMetadataListener.onVideoFrameAboutToBeRendered(
/* presentationTimeUs= */ bufferPresentationTimeUs, /* presentationTimeUs= */ framePresentationTimeUs,
/* releaseTimeNs= */ C.TIME_UNSET, /* releaseTimeNs= */ C.TIME_UNSET,
videoGraphOutputFormat, videoGraphOutputFormat,
/* mediaFormat= */ null); /* mediaFormat= */ null);
@ -504,8 +499,8 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
// The frame presentation time is relative to the start of the Composition and without the // The frame presentation time is relative to the start of the Composition and without the
// renderer offset // renderer offset
lastOutputBufferPresentationTimeUs = bufferPresentationTimeUs; lastOutputFramePresentationTimeUs = framePresentationTimeUs;
StreamChangeInfo streamChangeInfo = pendingStreamChanges.pollFloor(bufferPresentationTimeUs); StreamChangeInfo streamChangeInfo = pendingStreamChanges.pollFloor(framePresentationTimeUs);
if (streamChangeInfo != null) { if (streamChangeInfo != null) {
outputStreamStartPositionUs = streamChangeInfo.startPositionUs; outputStreamStartPositionUs = streamChangeInfo.startPositionUs;
outputStreamFirstFrameReleaseInstruction = streamChangeInfo.firstFrameReleaseInstruction; outputStreamFirstFrameReleaseInstruction = streamChangeInfo.firstFrameReleaseInstruction;
@ -513,8 +508,8 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
} }
defaultVideoSink.handleInputFrame(framePresentationTimeUs, videoFrameHandler); defaultVideoSink.handleInputFrame(framePresentationTimeUs, videoFrameHandler);
boolean isLastFrame = boolean isLastFrame =
finalBufferPresentationTimeUs != C.TIME_UNSET finalFramePresentationTimeUs != C.TIME_UNSET
&& bufferPresentationTimeUs >= finalBufferPresentationTimeUs; && framePresentationTimeUs >= finalFramePresentationTimeUs;
if (isLastFrame) { if (isLastFrame) {
signalEndOfVideoGraphOutputStream(); signalEndOfVideoGraphOutputStream();
} }
@ -664,8 +659,8 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
outputStreamFirstFrameReleaseInstruction = streamChangeInfo.firstFrameReleaseInstruction; outputStreamFirstFrameReleaseInstruction = streamChangeInfo.firstFrameReleaseInstruction;
onOutputStreamChanged(); onOutputStreamChanged();
} }
lastOutputBufferPresentationTimeUs = C.TIME_UNSET; lastOutputFramePresentationTimeUs = C.TIME_UNSET;
finalBufferPresentationTimeUs = C.TIME_UNSET; finalFramePresentationTimeUs = C.TIME_UNSET;
hasSignaledEndOfVideoGraphOutputStream = false; hasSignaledEndOfVideoGraphOutputStream = 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.
@ -690,11 +685,6 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
defaultVideoSink.setPlaybackSpeed(speed); defaultVideoSink.setPlaybackSpeed(speed);
} }
private void setBufferTimestampAdjustment(long bufferTimestampAdjustmentUs) {
this.bufferTimestampAdjustmentUs = bufferTimestampAdjustmentUs;
defaultVideoSink.setBufferTimestampAdjustmentUs(bufferTimestampAdjustmentUs);
}
private void setChangeFrameRateStrategy( private void setChangeFrameRateStrategy(
@C.VideoChangeFrameRateStrategy int changeFrameRateStrategy) { @C.VideoChangeFrameRateStrategy int changeFrameRateStrategy) {
defaultVideoSink.setChangeFrameRateStrategy(changeFrameRateStrategy); defaultVideoSink.setChangeFrameRateStrategy(changeFrameRateStrategy);
@ -733,10 +723,8 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
private @InputType int inputType; private @InputType int inputType;
private long inputBufferTimestampAdjustmentUs; private long inputBufferTimestampAdjustmentUs;
/** /** The frame presentation timestamp, in microseconds, of the most recently registered frame. */
* The buffer presentation timestamp, in microseconds, of the most recently registered frame. private long lastFramePresentationTimeUs;
*/
private long lastBufferPresentationTimeUs;
private VideoSink.Listener listener; private VideoSink.Listener listener;
private Executor listenerExecutor; private Executor listenerExecutor;
@ -752,7 +740,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
videoFrameProcessorMaxPendingFrameCount = videoFrameProcessorMaxPendingFrameCount =
getMaxPendingFramesCountForMediaCodecDecoders(context); getMaxPendingFramesCountForMediaCodecDecoders(context);
videoEffects = ImmutableList.of(); videoEffects = ImmutableList.of();
lastBufferPresentationTimeUs = C.TIME_UNSET; lastFramePresentationTimeUs = C.TIME_UNSET;
listener = VideoSink.Listener.NO_OP; listener = VideoSink.Listener.NO_OP;
listenerExecutor = NO_OP_EXECUTOR; listenerExecutor = NO_OP_EXECUTOR;
} }
@ -796,10 +784,10 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
} }
// Resignal EOS only for the last item. // Resignal EOS only for the last item.
boolean needsResignalEndOfCurrentInputStream = signaledEndOfStream; boolean needsResignalEndOfCurrentInputStream = signaledEndOfStream;
long replayedPresentationTimeUs = lastOutputBufferPresentationTimeUs; long replayedPresentationTimeUs = lastOutputFramePresentationTimeUs;
PlaybackVideoGraphWrapper.this.flush(/* resetPosition= */ false); PlaybackVideoGraphWrapper.this.flush(/* resetPosition= */ false);
checkNotNull(videoGraph).redraw(); checkNotNull(videoGraph).redraw();
lastOutputBufferPresentationTimeUs = replayedPresentationTimeUs; lastOutputFramePresentationTimeUs = replayedPresentationTimeUs;
if (needsResignalEndOfCurrentInputStream) { if (needsResignalEndOfCurrentInputStream) {
signalEndOfCurrentInputStream(); signalEndOfCurrentInputStream();
} }
@ -810,7 +798,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
if (isInitialized()) { if (isInitialized()) {
checkNotNull(videoGraph).flush(); checkNotNull(videoGraph).flush();
} }
lastBufferPresentationTimeUs = C.TIME_UNSET; lastFramePresentationTimeUs = C.TIME_UNSET;
PlaybackVideoGraphWrapper.this.flush(resetPosition); PlaybackVideoGraphWrapper.this.flush(resetPosition);
signaledEndOfStream = false; signaledEndOfStream = false;
// 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
@ -827,8 +815,8 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
@Override @Override
public void signalEndOfCurrentInputStream() { public void signalEndOfCurrentInputStream() {
finalBufferPresentationTimeUs = lastBufferPresentationTimeUs; finalFramePresentationTimeUs = lastFramePresentationTimeUs;
if (lastOutputBufferPresentationTimeUs >= finalBufferPresentationTimeUs) { if (lastOutputFramePresentationTimeUs >= finalFramePresentationTimeUs) {
PlaybackVideoGraphWrapper.this.signalEndOfVideoGraphOutputStream(); PlaybackVideoGraphWrapper.this.signalEndOfVideoGraphOutputStream();
} }
} }
@ -860,17 +848,23 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
this.videoEffects = ImmutableList.copyOf(videoEffects); this.videoEffects = ImmutableList.copyOf(videoEffects);
this.inputType = inputType; this.inputType = inputType;
this.inputFormat = format; this.inputFormat = format;
finalBufferPresentationTimeUs = C.TIME_UNSET; finalFramePresentationTimeUs = C.TIME_UNSET;
hasSignaledEndOfVideoGraphOutputStream = false; hasSignaledEndOfVideoGraphOutputStream = false;
registerInputStream(format); registerInputStream(format);
// Input timestamps should always be positive because they are offset by ExoPlayer. Adding a long fromTimestampUs;
// stream change info to the queue with timestamp 0 should therefore always apply it as long if (lastFramePresentationTimeUs == C.TIME_UNSET) {
// as it is the only one in the queue. // Add a stream change info to the queue with a large negative timestamp to always apply it
long fromTimestampUs = // as long as it is the only one in the queue.
lastBufferPresentationTimeUs == C.TIME_UNSET ? 0 : lastBufferPresentationTimeUs + 1; fromTimestampUs = Long.MIN_VALUE / 2;
} else {
fromTimestampUs = lastFramePresentationTimeUs + 1;
}
pendingStreamChanges.add( pendingStreamChanges.add(
fromTimestampUs, fromTimestampUs,
new StreamChangeInfo(startPositionUs, firstFrameReleaseInstruction, fromTimestampUs)); new StreamChangeInfo(
/* startPositionUs= */ startPositionUs + inputBufferTimestampAdjustmentUs,
firstFrameReleaseInstruction,
fromTimestampUs));
} }
@Override @Override
@ -951,11 +945,6 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
@Override @Override
public void setBufferTimestampAdjustmentUs(long bufferTimestampAdjustmentUs) { public void setBufferTimestampAdjustmentUs(long bufferTimestampAdjustmentUs) {
inputBufferTimestampAdjustmentUs = bufferTimestampAdjustmentUs; inputBufferTimestampAdjustmentUs = bufferTimestampAdjustmentUs;
// The buffer timestamp adjustment is only allowed to change after a flush to make sure that
// the buffer timestamps are increasing. We can update the buffer timestamp adjustment
// directly at the output of the VideoGraph because no frame has been input yet following the
// flush.
PlaybackVideoGraphWrapper.this.setBufferTimestampAdjustment(inputBufferTimestampAdjustmentUs);
} }
@Override @Override
@ -978,7 +967,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
@Override @Override
public boolean handleInputFrame( public boolean handleInputFrame(
long framePresentationTimeUs, VideoFrameHandler videoFrameHandler) { long bufferPresentationTimeUs, VideoFrameHandler videoFrameHandler) {
checkState(isInitialized()); checkState(isInitialized());
if (!shouldRenderToInputVideoSink()) { if (!shouldRenderToInputVideoSink()) {
@ -999,10 +988,11 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
// duration of the first video. Thus this correction is needed to account for the different // duration of the first video. Thus this correction is needed to account for the different
// handling of presentation timestamps in ExoPlayer and VideoFrameProcessor. // handling of presentation timestamps in ExoPlayer and VideoFrameProcessor.
// //
// inputBufferTimestampAdjustmentUs adjusts the frame presentation time (which is relative to // inputBufferTimestampAdjustmentUs adjusts the buffer timestamp (that corresponds to the
// the start of a composition) to the buffer timestamp (that corresponds to the player // player position) to the frame presentation time (which is relative to the start of a
// position). // composition).
lastBufferPresentationTimeUs = framePresentationTimeUs - inputBufferTimestampAdjustmentUs; long framePresentationTimeUs = bufferPresentationTimeUs + inputBufferTimestampAdjustmentUs;
lastFramePresentationTimeUs = framePresentationTimeUs;
// 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.
@ -1011,25 +1001,29 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
} }
@Override @Override
public boolean handleInputBitmap(Bitmap inputBitmap, TimestampIterator timestampIterator) { public boolean handleInputBitmap(
Bitmap inputBitmap, TimestampIterator bufferTimestampIterator) {
checkState(isInitialized()); checkState(isInitialized());
if (!shouldRenderToInputVideoSink() if (!shouldRenderToInputVideoSink()) {
|| !checkNotNull(videoGraph) return false;
.queueInputBitmap(inputIndex, inputBitmap, timestampIterator)) { }
TimestampIterator frameTimestampIterator =
new ShiftingTimestampIterator(bufferTimestampIterator, inputBufferTimestampAdjustmentUs);
if (!checkNotNull(videoGraph)
.queueInputBitmap(inputIndex, inputBitmap, frameTimestampIterator)) {
return false; return false;
} }
// TimestampIterator generates frame time. long lastFramePresentationTimeUs = frameTimestampIterator.getLastTimestampUs();
long lastBufferPresentationTimeUs = checkState(lastFramePresentationTimeUs != C.TIME_UNSET);
timestampIterator.getLastTimestampUs() - inputBufferTimestampAdjustmentUs; this.lastFramePresentationTimeUs = lastFramePresentationTimeUs;
checkState(lastBufferPresentationTimeUs != C.TIME_UNSET);
this.lastBufferPresentationTimeUs = lastBufferPresentationTimeUs;
return true; return true;
} }
@Override @Override
public void render(long positionUs, long elapsedRealtimeUs) throws VideoSinkException { public void render(long positionUs, long elapsedRealtimeUs) throws VideoSinkException {
PlaybackVideoGraphWrapper.this.render(positionUs, elapsedRealtimeUs); PlaybackVideoGraphWrapper.this.render(
/* positionUs= */ positionUs + inputBufferTimestampAdjustmentUs, elapsedRealtimeUs);
} }
@Override @Override
@ -1149,6 +1143,40 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
} }
} }
private static final class ShiftingTimestampIterator implements TimestampIterator {
private final TimestampIterator timestampIterator;
private final long shift;
public ShiftingTimestampIterator(TimestampIterator timestampIterator, long shift) {
this.timestampIterator = timestampIterator;
this.shift = shift;
}
@Override
public boolean hasNext() {
return timestampIterator.hasNext();
}
@Override
public long next() {
return timestampIterator.next() + shift;
}
@Override
public TimestampIterator copyOf() {
return new ShiftingTimestampIterator(timestampIterator.copyOf(), shift);
}
@Override
public long getLastTimestampUs() {
long unshiftedLastTimestampUs = timestampIterator.getLastTimestampUs();
return unshiftedLastTimestampUs == C.TIME_UNSET
? C.TIME_UNSET
: unshiftedLastTimestampUs + shift;
}
}
/** Delays reflection for loading a {@link VideoGraph.Factory SingleInputVideoGraph} instance. */ /** Delays reflection for loading a {@link VideoGraph.Factory SingleInputVideoGraph} instance. */
private static final class ReflectiveSingleInputVideoGraphFactory implements VideoGraph.Factory { private static final class ReflectiveSingleInputVideoGraphFactory implements VideoGraph.Factory {

View File

@ -185,8 +185,12 @@ import androidx.media3.exoplayer.ExoPlaybackException;
videoFrameReleaseControl.onStreamChanged(firstFrameReleaseInstruction); videoFrameReleaseControl.onStreamChanged(firstFrameReleaseInstruction);
outputStreamStartPositionUs = streamStartPositionUs; outputStreamStartPositionUs = streamStartPositionUs;
} else { } else {
// Add a start position to the queue with a large negative timestamp to always apply it as
// long as it is the only one in the queue.
streamStartPositionsUs.add( streamStartPositionsUs.add(
latestInputPresentationTimeUs == C.TIME_UNSET ? 0 : latestInputPresentationTimeUs + 1, latestInputPresentationTimeUs == C.TIME_UNSET
? Long.MIN_VALUE / 2
: latestInputPresentationTimeUs + 1,
streamStartPositionUs); streamStartPositionUs);
} }
} }

View File

@ -277,12 +277,12 @@ public interface VideoSink {
* <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, long, int, List) signaled}. * Format, long, int, List) signaled}.
* *
* @param framePresentationTimeUs The frame's presentation time, in microseconds. * @param bufferPresentationTimeUs The buffer presentation time, in microseconds.
* @param videoFrameHandler The {@link VideoFrameHandler} used to handle the input frame. * @param videoFrameHandler The {@link VideoFrameHandler} used to handle the input frame.
* @return Whether the frame was handled successfully. If {@code false}, the caller can try again * @return Whether the frame was handled successfully. If {@code false}, the caller can try again
* later. * later.
*/ */
boolean handleInputFrame(long framePresentationTimeUs, VideoFrameHandler videoFrameHandler); boolean handleInputFrame(long bufferPresentationTimeUs, VideoFrameHandler videoFrameHandler);
/** /**
* Handles an input {@link Bitmap}. * Handles an input {@link Bitmap}.
@ -291,12 +291,12 @@ public interface VideoSink {
* Format, long, int, List) signaled}. * Format, long, int, 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 bufferTimestampIterator The buffer presentation times within the current stream that the
* at. The timestamps should be monotonically increasing. * bitmap should be shown at. The timestamps should be monotonically increasing.
* @return Whether the bitmap was queued successfully. If {@code false}, the caller can try again * @return Whether the bitmap was queued successfully. If {@code false}, the caller can try again
* later. * later.
*/ */
boolean handleInputBitmap(Bitmap inputBitmap, TimestampIterator timestampIterator); boolean handleInputBitmap(Bitmap inputBitmap, TimestampIterator bufferTimestampIterator);
/** /**
* Incrementally renders processed video frames to the output surface. * Incrementally renders processed video frames to the output surface.

View File

@ -1,41 +0,0 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.video;
import android.view.Surface;
import androidx.media3.common.util.Size;
/** A provider of {@link VideoSink VideoSinks}. */
/* package */ interface VideoSinkProvider {
/**
* Returns the {@link VideoSink} to forward video frames for processing.
*
* @param inputIndex The index of the {@link VideoSink}.
* @return The {@link VideoSink} at the given index.
*/
VideoSink getSink(int inputIndex);
/** Sets the output surface info. */
void setOutputSurfaceInfo(Surface outputSurface, Size outputResolution);
/** Clears the set output surface info. */
void clearOutputSurfaceInfo();
/** Releases the sink provider. */
void release();
}

View File

@ -349,9 +349,8 @@ public class EffectPlaybackPixelTest {
} }
}); });
player.setVideoFrameMetadataListener( player.setVideoFrameMetadataListener(
(bufferPresentationTimeUs, releaseTimeNs, format, mediaFormat) -> { (presentationTimeUs, releaseTimeNs, format, mediaFormat) -> {
// The buffer presentation time is offset with rendererOffset. if (presentationTimeUs != 0) {
if (bufferPresentationTimeUs != 1_000_000_000_000L) {
return; return;
} }
@ -359,7 +358,7 @@ public class EffectPlaybackPixelTest {
// Render the current frame, and redraw a frame with some delay. This is to ensure // Render the current frame, and redraw a frame with some delay. This is to ensure
// that the first frame is rendered with the original effect, and the second // that the first frame is rendered with the original effect, and the second
// frame is rendered with the new effect. Following this call, the first frame // frame is rendered with the new effect. Following this call, the first frame
// will be rendered twicw. // will be rendered twice.
mainHandler.postDelayed( mainHandler.postDelayed(
() -> { () -> {
contrast.changeContrast(-0.8f); contrast.changeContrast(-0.8f);

View File

@ -233,9 +233,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/ */
@Override @Override
public boolean handleInputFrame( public boolean handleInputFrame(
long framePresentationTimeUs, VideoFrameHandler videoFrameHandler) { long bufferPresentationTimeUs, VideoFrameHandler videoFrameHandler) {
return videoSink != null return videoSink != null
&& videoSink.handleInputFrame(framePresentationTimeUs, videoFrameHandler); && videoSink.handleInputFrame(bufferPresentationTimeUs, videoFrameHandler);
} }
/** /**
@ -245,8 +245,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* sink} is {@code null}. * sink} is {@code null}.
*/ */
@Override @Override
public boolean handleInputBitmap(Bitmap inputBitmap, TimestampIterator timestampIterator) { public boolean handleInputBitmap(Bitmap inputBitmap, TimestampIterator bufferTimestampIterator) {
return videoSink != null && videoSink.handleInputBitmap(inputBitmap, timestampIterator); return videoSink != null && videoSink.handleInputBitmap(inputBitmap, bufferTimestampIterator);
} }
/** /**

View File

@ -500,7 +500,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private long streamStartPositionUs; private long streamStartPositionUs;
private boolean mayRenderStartOfStream; private boolean mayRenderStartOfStream;
private @VideoSink.FirstFrameReleaseInstruction int nextFirstFrameReleaseInstruction; private @VideoSink.FirstFrameReleaseInstruction int nextFirstFrameReleaseInstruction;
private long offsetToCompositionTimeUs;
private @MonotonicNonNull WakeupListener wakeupListener; private @MonotonicNonNull WakeupListener wakeupListener;
public SequenceImageRenderer( public SequenceImageRenderer(
@ -598,7 +597,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// The media item might have been repeated in the sequence. // The media item might have been repeated in the sequence.
int mediaItemIndex = getTimeline().getIndexOfPeriod(mediaPeriodId.periodUid); int mediaItemIndex = getTimeline().getIndexOfPeriod(mediaPeriodId.periodUid);
currentEditedMediaItem = sequence.editedMediaItems.get(mediaItemIndex); currentEditedMediaItem = sequence.editedMediaItems.get(mediaItemIndex);
offsetToCompositionTimeUs = getOffsetToCompositionTimeUs(sequence, mediaItemIndex, offsetUs); long offsetToCompositionTimeUs =
getOffsetToCompositionTimeUs(sequence, mediaItemIndex, offsetUs);
videoSink.setBufferTimestampAdjustmentUs(offsetToCompositionTimeUs); videoSink.setBufferTimestampAdjustmentUs(offsetToCompositionTimeUs);
timestampIterator = createTimestampIterator(/* positionUs= */ startPositionUs); timestampIterator = createTimestampIterator(/* positionUs= */ startPositionUs);
videoEffects = currentEditedMediaItem.effects.videoEffects; videoEffects = currentEditedMediaItem.effects.videoEffects;
@ -663,14 +663,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
private ConstantRateTimestampIterator createTimestampIterator(long positionUs) { private ConstantRateTimestampIterator createTimestampIterator(long positionUs) {
long streamOffsetUs = getStreamOffsetUs();
long imageBaseTimestampUs = streamOffsetUs + offsetToCompositionTimeUs;
long positionWithinImage = positionUs - streamOffsetUs;
long firstBitmapTimeUs = imageBaseTimestampUs + positionWithinImage;
long lastBitmapTimeUs = long lastBitmapTimeUs =
imageBaseTimestampUs + checkNotNull(currentEditedMediaItem).getPresentationDurationUs(); getStreamOffsetUs() + checkNotNull(currentEditedMediaItem).getPresentationDurationUs();
return new ConstantRateTimestampIterator( return new ConstantRateTimestampIterator(
/* startPositionUs= */ firstBitmapTimeUs, /* startPositionUs= */ positionUs,
/* endPositionUs= */ lastBitmapTimeUs, /* endPositionUs= */ lastBitmapTimeUs,
DEFAULT_FRAME_RATE); DEFAULT_FRAME_RATE);
} }