From 06b94f544878a759d8e4a5a6276b95933c646298 Mon Sep 17 00:00:00 2001 From: kimvde Date: Wed, 4 Dec 2024 04:26:09 -0800 Subject: [PATCH] DefaultVideoSink: implement handleInputFrame() and listeners With this CL, PlaybackVideoGraphWrapper doesn't call the VideoFrameRenderControl directly anymore (which is one of the goals of DefaultVideoSink). PiperOrigin-RevId: 702673821 --- .../exoplayer/video/DefaultVideoSink.java | 78 ++++++++++++-- .../video/PlaybackVideoGraphWrapper.java | 102 +++++++++--------- 2 files changed, 118 insertions(+), 62 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DefaultVideoSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DefaultVideoSink.java index 4dbb5ef80c..48f513512c 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DefaultVideoSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DefaultVideoSink.java @@ -23,12 +23,18 @@ import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Effect; import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.VideoSize; +import androidx.media3.common.util.Clock; import androidx.media3.common.util.Size; import androidx.media3.common.util.TimestampIterator; import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.Renderer; +import java.util.ArrayDeque; import java.util.List; +import java.util.Queue; import java.util.concurrent.Executor; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * The default {@link VideoSink} implementation. This implementation renders video frames to an @@ -39,7 +45,7 @@ import java.util.concurrent.Executor; * * *

The {@linkplain #getInputSurface() input} and {@linkplain #setOutputSurfaceInfo(Surface, Size) @@ -48,19 +54,30 @@ import java.util.concurrent.Executor; /* package */ final class DefaultVideoSink implements VideoSink { private final VideoFrameReleaseControl videoFrameReleaseControl; + private final Clock clock; private final VideoFrameRenderControl videoFrameRenderControl; + private final Queue videoFrameHandlers; @Nullable private Surface outputSurface; private Format inputFormat; private long streamStartPositionUs; + private long bufferTimestampAdjustmentUs; + private Listener listener; + private Executor listenerExecutor; + private VideoFrameMetadataListener videoFrameMetadataListener; - public DefaultVideoSink( - VideoFrameReleaseControl videoFrameReleaseControl, - VideoFrameRenderControl videoFrameRenderControl) { + public DefaultVideoSink(VideoFrameReleaseControl videoFrameReleaseControl, Clock clock) { this.videoFrameReleaseControl = videoFrameReleaseControl; - this.videoFrameRenderControl = videoFrameRenderControl; + videoFrameReleaseControl.setClock(clock); + this.clock = clock; + videoFrameRenderControl = + new VideoFrameRenderControl(new FrameRendererImpl(), videoFrameReleaseControl); + videoFrameHandlers = new ArrayDeque<>(); inputFormat = new Format.Builder().build(); streamStartPositionUs = C.TIME_UNSET; + listener = Listener.NO_OP; + listenerExecutor = runnable -> {}; + videoFrameMetadataListener = (presentationTimeUs, releaseTimeNs, format, mediaFormat) -> {}; } @Override @@ -85,7 +102,8 @@ import java.util.concurrent.Executor; @Override public void setListener(Listener listener, Executor executor) { - throw new UnsupportedOperationException(); + this.listener = listener; + this.listenerExecutor = executor; } @Override @@ -104,6 +122,7 @@ import java.util.concurrent.Executor; videoFrameReleaseControl.reset(); } videoFrameRenderControl.flush(); + videoFrameHandlers.clear(); } @Override @@ -133,7 +152,7 @@ import java.util.concurrent.Executor; @Override public void setVideoFrameMetadataListener(VideoFrameMetadataListener videoFrameMetadataListener) { - throw new UnsupportedOperationException(); + this.videoFrameMetadataListener = videoFrameMetadataListener; } @Override @@ -168,6 +187,7 @@ import java.util.concurrent.Executor; videoFrameRenderControl.onStreamStartPositionChanged(streamStartPositionUs); this.streamStartPositionUs = streamStartPositionUs; } + this.bufferTimestampAdjustmentUs = bufferTimestampAdjustmentUs; } @Override @@ -206,7 +226,10 @@ import java.util.concurrent.Executor; @Override public boolean handleInputFrame( long framePresentationTimeUs, boolean isLastFrame, VideoFrameHandler videoFrameHandler) { - throw new UnsupportedOperationException(); + videoFrameHandlers.add(videoFrameHandler); + long bufferPresentationTimeUs = framePresentationTimeUs - bufferTimestampAdjustmentUs; + videoFrameRenderControl.onFrameAvailableForRendering(bufferPresentationTimeUs); + return true; } /** @@ -245,4 +268,43 @@ import java.util.concurrent.Executor; @Override public void release() {} + + private final class FrameRendererImpl implements VideoFrameRenderControl.FrameRenderer { + + private @MonotonicNonNull Format outputFormat; + + @Override + public void onVideoSizeChanged(VideoSize videoSize) { + outputFormat = + new Format.Builder() + .setWidth(videoSize.width) + .setHeight(videoSize.height) + .setSampleMimeType(MimeTypes.VIDEO_RAW) + .build(); + listenerExecutor.execute(() -> listener.onVideoSizeChanged(DefaultVideoSink.this, videoSize)); + } + + @Override + public void renderFrame( + long renderTimeNs, long bufferPresentationTimeUs, boolean isFirstFrame) { + if (isFirstFrame && outputSurface != null) { + listenerExecutor.execute(() -> listener.onFirstFrameRendered(DefaultVideoSink.this)); + } + // TODO - b/292111083: outputFormat is initialized after the first frame is rendered because + // onVideoSizeChanged is announced after the first frame is available for rendering. + Format format = outputFormat == null ? new Format.Builder().build() : outputFormat; + videoFrameMetadataListener.onVideoFrameAboutToBeRendered( + /* presentationTimeUs= */ bufferPresentationTimeUs, + /* releaseTimeNs= */ clock.nanoTime(), + format, + /* mediaFormat= */ null); + videoFrameHandlers.remove().render(renderTimeNs); + } + + @Override + public void dropFrame() { + listenerExecutor.execute(() -> listener.onFrameDropped(DefaultVideoSink.this)); + videoFrameHandlers.remove().skip(); + } + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java index 23ba850017..7df19ff8b2 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java @@ -15,6 +15,7 @@ */ package androidx.media3.exoplayer.video; +import static androidx.media3.common.VideoFrameProcessor.DROP_OUTPUT_FRAME; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; @@ -36,7 +37,6 @@ import androidx.media3.common.ColorInfo; import androidx.media3.common.DebugViewProvider; import androidx.media3.common.Effect; import androidx.media3.common.Format; -import androidx.media3.common.MimeTypes; import androidx.media3.common.PreviewingVideoGraph; import androidx.media3.common.SurfaceInfo; import androidx.media3.common.VideoFrameProcessingException; @@ -235,15 +235,14 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video */ private final TimedValueQueue streamStartPositionsUs; - private final VideoFrameRenderControl videoFrameRenderControl; private final PreviewingVideoGraph.Factory previewingVideoGraphFactory; private final List compositionEffects; private final VideoSink defaultVideoSink; + private final VideoSink.VideoFrameHandler videoFrameHandler; private final Clock clock; private final CopyOnWriteArraySet listeners; private Format videoGraphOutputFormat; - private @MonotonicNonNull VideoFrameMetadataListener videoFrameMetadataListener; private @MonotonicNonNull HandlerWrapper handler; private @MonotonicNonNull PreviewingVideoGraph videoGraph; private long outputStreamStartPositionUs; @@ -268,18 +267,26 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video context = builder.context; inputVideoSink = new InputVideoSink(context); streamStartPositionsUs = new TimedValueQueue<>(); - clock = builder.clock; - VideoFrameReleaseControl videoFrameReleaseControl = builder.videoFrameReleaseControl; - videoFrameReleaseControl.setClock(clock); - videoFrameRenderControl = - new VideoFrameRenderControl(new FrameRendererImpl(), videoFrameReleaseControl); previewingVideoGraphFactory = checkStateNotNull(builder.previewingVideoGraphFactory); compositionEffects = builder.compositionEffects; - defaultVideoSink = new DefaultVideoSink(videoFrameReleaseControl, videoFrameRenderControl); + clock = builder.clock; + defaultVideoSink = new DefaultVideoSink(builder.videoFrameReleaseControl, clock); + videoFrameHandler = + new VideoSink.VideoFrameHandler() { + @Override + public void render(long renderTimestampNs) { + checkStateNotNull(videoGraph).renderOutputFrame(renderTimestampNs); + } + + @Override + public void skip() { + checkStateNotNull(videoGraph).renderOutputFrame(DROP_OUTPUT_FRAME); + } + }; listeners = new CopyOnWriteArraySet<>(); - state = STATE_CREATED; + listeners.add(inputVideoSink); videoGraphOutputFormat = new Format.Builder().build(); - addListener(inputVideoSink); + state = STATE_CREATED; finalBufferPresentationTimeUs = C.TIME_UNSET; } @@ -334,11 +341,9 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video if (state == STATE_RELEASED) { return; } - if (handler != null) { handler.removeCallbacksAndMessages(/* token= */ null); } - if (videoGraph != null) { videoGraph.release(); } @@ -380,12 +385,14 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video if (newOutputStreamStartPositionUs != null && newOutputStreamStartPositionUs != outputStreamStartPositionUs) { defaultVideoSink.setStreamTimestampInfo( - newOutputStreamStartPositionUs, /* unused */ C.TIME_UNSET, /* unused */ C.TIME_UNSET); + newOutputStreamStartPositionUs, bufferTimestampAdjustmentUs, /* unused */ C.TIME_UNSET); outputStreamStartPositionUs = newOutputStreamStartPositionUs; } - videoFrameRenderControl.onFrameAvailableForRendering(bufferPresentationTimeUs); - if (finalBufferPresentationTimeUs != C.TIME_UNSET - && bufferPresentationTimeUs >= finalBufferPresentationTimeUs) { + boolean isLastFrame = + finalBufferPresentationTimeUs != C.TIME_UNSET + && bufferPresentationTimeUs >= finalBufferPresentationTimeUs; + defaultVideoSink.handleInputFrame(framePresentationTimeUs, isLastFrame, videoFrameHandler); + if (isLastFrame) { // TODO b/257464707 - Support extensively modified media. defaultVideoSink.signalEndOfCurrentInputStream(); hasSignaledEndOfCurrentInputStream = true; @@ -438,6 +445,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video } catch (VideoFrameProcessingException e) { throw new VideoSink.VideoSinkException(e, sourceFormat); } + defaultVideoSink.setListener(new DefaultVideoSinkListener(), /* executor= */ handler::post); defaultVideoSink.initialize(sourceFormat); state = STATE_INITIALIZED; return videoGraph.getProcessor(/* inputIndex= */ 0); @@ -496,7 +504,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video long lastStartPositionUs = checkNotNull(streamStartPositionsUs.pollFirst()); // defaultVideoSink should use the latest startPositionUs if none is passed after flushing. defaultVideoSink.setStreamTimestampInfo( - lastStartPositionUs, /* unused */ C.TIME_UNSET, /* unused */ C.TIME_UNSET); + lastStartPositionUs, bufferTimestampAdjustmentUs, /* unused */ C.TIME_UNSET); } finalBufferPresentationTimeUs = C.TIME_UNSET; hasSignaledEndOfCurrentInputStream = false; @@ -507,7 +515,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video private void setVideoFrameMetadataListener( VideoFrameMetadataListener videoFrameMetadataListener) { - this.videoFrameMetadataListener = videoFrameMetadataListener; + defaultVideoSink.setVideoFrameMetadataListener(videoFrameMetadataListener); } private void setPlaybackSpeed(float speed) { @@ -516,6 +524,8 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video private void setBufferTimestampAdjustment(long bufferTimestampAdjustmentUs) { this.bufferTimestampAdjustmentUs = bufferTimestampAdjustmentUs; + defaultVideoSink.setStreamTimestampInfo( + outputStreamStartPositionUs, bufferTimestampAdjustmentUs, /* unused */ C.TIME_UNSET); } private static ColorInfo getAdjustedInputColorInfo(@Nullable ColorInfo inputColorInfo) { @@ -854,51 +864,35 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video } } - private final class FrameRendererImpl implements VideoFrameRenderControl.FrameRenderer { - - private @MonotonicNonNull Format renderedFormat; + private final class DefaultVideoSinkListener implements VideoSink.Listener { @Override - public void onVideoSizeChanged(VideoSize videoSize) { - renderedFormat = - new Format.Builder() - .setWidth(videoSize.width) - .setHeight(videoSize.height) - .setSampleMimeType(MimeTypes.VIDEO_RAW) - .build(); + public void onFirstFrameRendered(VideoSink videoSink) { + for (PlaybackVideoGraphWrapper.Listener listener : listeners) { + listener.onFirstFrameRendered(PlaybackVideoGraphWrapper.this); + } + } + + @Override + public void onFrameDropped(VideoSink videoSink) { + for (PlaybackVideoGraphWrapper.Listener listener : listeners) { + listener.onFrameDropped(PlaybackVideoGraphWrapper.this); + } + } + + @Override + public void onVideoSizeChanged(VideoSink videoSink, VideoSize videoSize) { for (PlaybackVideoGraphWrapper.Listener listener : listeners) { listener.onVideoSizeChanged(PlaybackVideoGraphWrapper.this, videoSize); } } @Override - public void renderFrame( - long renderTimeNs, long bufferPresentationTimeUs, boolean isFirstFrame) { - if (isFirstFrame && currentSurfaceAndSize != null) { - for (PlaybackVideoGraphWrapper.Listener listener : listeners) { - listener.onFirstFrameRendered(PlaybackVideoGraphWrapper.this); - } - } - if (videoFrameMetadataListener != null) { - // TODO b/292111083 - renderedFormat is initialized after the first frame is rendered - // because onVideoSizeChanged is announced after the first frame is available for - // rendering. - Format format = renderedFormat == null ? new Format.Builder().build() : renderedFormat; - videoFrameMetadataListener.onVideoFrameAboutToBeRendered( - /* presentationTimeUs= */ bufferPresentationTimeUs, - clock.nanoTime(), - format, - /* mediaFormat= */ null); - } - checkStateNotNull(videoGraph).renderOutputFrame(renderTimeNs); - } - - @Override - public void dropFrame() { + public void onError(VideoSink videoSink, VideoSink.VideoSinkException videoSinkException) { for (PlaybackVideoGraphWrapper.Listener listener : listeners) { - listener.onFrameDropped(PlaybackVideoGraphWrapper.this); + listener.onError( + PlaybackVideoGraphWrapper.this, VideoFrameProcessingException.from(videoSinkException)); } - checkStateNotNull(videoGraph).renderOutputFrame(VideoFrameProcessor.DROP_OUTPUT_FRAME); } }