Move VideoSinkProvider.initialize to VideoSink

PiperOrigin-RevId: 627290721
This commit is contained in:
kimvde 2024-04-23 00:34:08 -07:00 committed by Copybara-Service
parent a43ffa8898
commit e3caed1441
5 changed files with 217 additions and 203 deletions

View File

@ -64,8 +64,8 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import org.checkerframework.checker.initialization.qual.Initialized;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Handles composition of video sinks. */ /** Handles composition of video sinks. */
@ -197,18 +197,17 @@ public final class CompositingVideoSinkProvider
private static final Executor NO_OP_EXECUTOR = runnable -> {}; private static final Executor NO_OP_EXECUTOR = runnable -> {};
private final Context context; private final Context context;
private final VideoSinkImpl videoSinkImpl;
private final PreviewingVideoGraph.Factory previewingVideoGraphFactory; private final PreviewingVideoGraph.Factory previewingVideoGraphFactory;
private final CopyOnWriteArraySet<CompositingVideoSinkProvider.Listener> listeners; private final CopyOnWriteArraySet<CompositingVideoSinkProvider.Listener> listeners;
private Clock clock; private Clock clock;
private List<Effect> videoEffects;
private @MonotonicNonNull VideoFrameReleaseControl videoFrameReleaseControl; private @MonotonicNonNull VideoFrameReleaseControl videoFrameReleaseControl;
private @MonotonicNonNull VideoFrameRenderControl videoFrameRenderControl; private @MonotonicNonNull VideoFrameRenderControl videoFrameRenderControl;
private @MonotonicNonNull Format outputFormat; private @MonotonicNonNull Format outputFormat;
private @MonotonicNonNull VideoFrameMetadataListener videoFrameMetadataListener; private @MonotonicNonNull VideoFrameMetadataListener videoFrameMetadataListener;
private @MonotonicNonNull HandlerWrapper handler; private @MonotonicNonNull HandlerWrapper handler;
private @MonotonicNonNull PreviewingVideoGraph videoGraph; private @MonotonicNonNull PreviewingVideoGraph videoGraph;
private @MonotonicNonNull VideoSinkImpl videoSinkImpl;
@Nullable private Pair<Surface, Size> currentSurfaceAndSize; @Nullable private Pair<Surface, Size> currentSurfaceAndSize;
private int pendingFlushCount; private int pendingFlushCount;
private @State int state; private @State int state;
@ -216,12 +215,13 @@ public final class CompositingVideoSinkProvider
private CompositingVideoSinkProvider(Builder builder) { private CompositingVideoSinkProvider(Builder builder) {
context = builder.context; context = builder.context;
videoEffects = ImmutableList.of(); videoSinkImpl = new VideoSinkImpl(context);
previewingVideoGraphFactory = checkStateNotNull(builder.previewingVideoGraphFactory); previewingVideoGraphFactory = checkStateNotNull(builder.previewingVideoGraphFactory);
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArraySet<>();
clock = Clock.DEFAULT; clock = Clock.DEFAULT;
state = STATE_CREATED; state = STATE_CREATED;
playbackSpeed = 1f; playbackSpeed = 1f;
addListener(videoSinkImpl);
} }
/** /**
@ -267,78 +267,22 @@ public final class CompositingVideoSinkProvider
@Override @Override
public void setVideoEffects(List<Effect> videoEffects) { public void setVideoEffects(List<Effect> videoEffects) {
this.videoEffects = videoEffects; videoSinkImpl.setVideoEffects(videoEffects);
if (isInitialized()) {
checkStateNotNull(videoSinkImpl).setVideoEffects(videoEffects);
}
} }
@Override @Override
public void setPendingVideoEffects(List<Effect> videoEffects) { public void setPendingVideoEffects(List<Effect> videoEffects) {
this.videoEffects = videoEffects; videoSinkImpl.setPendingVideoEffects(videoEffects);
if (isInitialized()) {
checkStateNotNull(videoSinkImpl).setPendingVideoEffects(videoEffects);
}
}
@Override
public void initialize(Format sourceFormat) throws VideoSink.VideoSinkException {
checkState(state == STATE_CREATED);
checkState(videoFrameRenderControl != null && videoFrameReleaseControl != null);
// Lazily initialize the handler here so it's initialized on the playback looper.
handler = clock.createHandler(checkStateNotNull(Looper.myLooper()), /* callback= */ null);
ColorInfo inputColorInfo = getAdjustedInputColorInfo(sourceFormat.colorInfo);
ColorInfo outputColorInfo = inputColorInfo;
if (inputColorInfo.colorTransfer == C.COLOR_TRANSFER_HLG && Util.SDK_INT < 34) {
// PQ SurfaceView output is supported from API 33, but HLG output is supported from API 34.
// Therefore, convert HLG to PQ below API 34, so that HLG input can be displayed properly on
// API 33.
outputColorInfo =
inputColorInfo.buildUpon().setColorTransfer(C.COLOR_TRANSFER_ST2084).build();
}
try {
@SuppressWarnings("nullness:assignment")
VideoGraph.@Initialized Listener thisRef = this;
videoGraph =
previewingVideoGraphFactory.create(
context,
outputColorInfo,
DebugViewProvider.NONE,
/* listener= */ thisRef,
/* listenerExecutor= */ handler::post,
/* compositionEffects= */ ImmutableList.of(),
/* initialTimestampOffsetUs= */ 0);
if (currentSurfaceAndSize != null) {
Surface surface = currentSurfaceAndSize.first;
Size size = currentSurfaceAndSize.second;
maybeSetOutputSurfaceInfo(surface, size.getWidth(), size.getHeight());
}
videoSinkImpl = new VideoSinkImpl(context, videoGraph);
} catch (VideoFrameProcessingException e) {
throw new VideoSink.VideoSinkException(e, sourceFormat);
}
if (!videoEffects.isEmpty()) {
videoSinkImpl.setVideoEffects(videoEffects);
}
addListener(videoSinkImpl);
state = STATE_INITIALIZED;
}
@Override
public boolean isInitialized() {
return state == STATE_INITIALIZED;
} }
@Override @Override
public VideoSink getSink() { public VideoSink getSink() {
return checkStateNotNull(videoSinkImpl); return videoSinkImpl;
} }
@Override @Override
public void setStreamOffsetUs(long streamOffsetUs) { public void setStreamOffsetUs(long streamOffsetUs) {
checkStateNotNull(videoSinkImpl).setStreamOffsetUs(streamOffsetUs); videoSinkImpl.setStreamOffsetUs(streamOffsetUs);
} }
@Override @Override
@ -485,6 +429,50 @@ public final class CompositingVideoSinkProvider
// Internal methods // Internal methods
private VideoFrameProcessor initialize(Format sourceFormat) throws VideoSink.VideoSinkException {
checkState(state == STATE_CREATED);
checkState(videoFrameRenderControl != null && videoFrameReleaseControl != null);
// Lazily initialize the handler here so it's initialized on the playback looper.
handler = clock.createHandler(checkStateNotNull(Looper.myLooper()), /* callback= */ null);
ColorInfo inputColorInfo = getAdjustedInputColorInfo(sourceFormat.colorInfo);
ColorInfo outputColorInfo = inputColorInfo;
if (inputColorInfo.colorTransfer == C.COLOR_TRANSFER_HLG && Util.SDK_INT < 34) {
// PQ SurfaceView output is supported from API 33, but HLG output is supported from API 34.
// Therefore, convert HLG to PQ below API 34, so that HLG input can be displayed properly on
// API 33.
outputColorInfo =
inputColorInfo.buildUpon().setColorTransfer(C.COLOR_TRANSFER_ST2084).build();
}
int videoGraphInputId;
try {
videoGraph =
previewingVideoGraphFactory.create(
context,
outputColorInfo,
DebugViewProvider.NONE,
/* listener= */ this,
/* listenerExecutor= */ handler::post,
/* compositionEffects= */ ImmutableList.of(),
/* initialTimestampOffsetUs= */ 0);
if (currentSurfaceAndSize != null) {
Surface surface = currentSurfaceAndSize.first;
Size size = currentSurfaceAndSize.second;
maybeSetOutputSurfaceInfo(surface, size.getWidth(), size.getHeight());
}
videoGraphInputId = videoGraph.registerInput();
} catch (VideoFrameProcessingException e) {
throw new VideoSink.VideoSinkException(e, sourceFormat);
}
state = STATE_INITIALIZED;
return videoGraph.getProcessor(videoGraphInputId);
}
private boolean isInitialized() {
return state == STATE_INITIALIZED;
}
private void maybeSetOutputSurfaceInfo(@Nullable Surface surface, int width, int height) { private void maybeSetOutputSurfaceInfo(@Nullable Surface surface, int width, int height) {
if (videoGraph != null) { if (videoGraph != null) {
// Update the surface on the video graph and the video frame release control together. // Update the surface on the video graph and the video frame release control together.
@ -551,11 +539,11 @@ public final class CompositingVideoSinkProvider
/** 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 VideoSinkImpl implements VideoSink, CompositingVideoSinkProvider.Listener { private final class VideoSinkImpl implements VideoSink, CompositingVideoSinkProvider.Listener {
private final Context context; private final Context context;
private final VideoFrameProcessor videoFrameProcessor;
private final int videoFrameProcessorMaxPendingFrameCount; private final int videoFrameProcessorMaxPendingFrameCount;
private final ArrayList<Effect> videoEffects; private final ArrayList<Effect> videoEffects;
@Nullable private Effect rotationEffect; @Nullable private Effect rotationEffect;
private @MonotonicNonNull VideoFrameProcessor videoFrameProcessor;
@Nullable private Format inputFormat; @Nullable private Format inputFormat;
private @InputType int inputType; private @InputType int inputType;
private long inputStreamOffsetUs; private long inputStreamOffsetUs;
@ -575,8 +563,7 @@ public final class CompositingVideoSinkProvider
private Executor listenerExecutor; private Executor listenerExecutor;
/** Creates a new instance. */ /** Creates a new instance. */
public VideoSinkImpl(Context context, PreviewingVideoGraph videoGraph) public VideoSinkImpl(Context context) {
throws VideoFrameProcessingException {
this.context = context; this.context = context;
// TODO b/226330223 - Investigate increasing frame count when frame dropping is // TODO b/226330223 - Investigate increasing frame count when frame dropping is
// allowed. // allowed.
@ -584,8 +571,6 @@ public final class CompositingVideoSinkProvider
// reduces decoder timeouts, and consider restoring. // reduces decoder timeouts, and consider restoring.
videoFrameProcessorMaxPendingFrameCount = videoFrameProcessorMaxPendingFrameCount =
Util.getMaxPendingFramesCountForMediaCodecDecoders(context); Util.getMaxPendingFramesCountForMediaCodecDecoders(context);
int videoGraphInputId = videoGraph.registerInput();
videoFrameProcessor = videoGraph.getProcessor(videoGraphInputId);
videoEffects = new ArrayList<>(); videoEffects = new ArrayList<>();
finalBufferPresentationTimeUs = C.TIME_UNSET; finalBufferPresentationTimeUs = C.TIME_UNSET;
@ -596,9 +581,23 @@ public final class CompositingVideoSinkProvider
// VideoSink impl // VideoSink impl
@Override
public void initialize(Format sourceFormat) throws VideoSinkException {
checkState(!isInitialized());
videoFrameProcessor = CompositingVideoSinkProvider.this.initialize(sourceFormat);
}
@Override
@EnsuresNonNullIf(result = true, expression = "videoFrameProcessor")
public boolean isInitialized() {
return videoFrameProcessor != null;
}
@Override @Override
public void flush() { public void flush() {
videoFrameProcessor.flush(); if (isInitialized()) {
videoFrameProcessor.flush();
}
hasRegisteredFirstInputStream = false; hasRegisteredFirstInputStream = false;
finalBufferPresentationTimeUs = C.TIME_UNSET; finalBufferPresentationTimeUs = C.TIME_UNSET;
lastBufferPresentationTimeUs = C.TIME_UNSET; lastBufferPresentationTimeUs = C.TIME_UNSET;
@ -611,17 +610,19 @@ public final class CompositingVideoSinkProvider
@Override @Override
public boolean isReady() { public boolean isReady() {
return CompositingVideoSinkProvider.this.isReady(); return isInitialized() && CompositingVideoSinkProvider.this.isReady();
} }
@Override @Override
public boolean isEnded() { public boolean isEnded() {
return finalBufferPresentationTimeUs != C.TIME_UNSET return isInitialized()
&& finalBufferPresentationTimeUs != C.TIME_UNSET
&& CompositingVideoSinkProvider.this.hasReleasedFrame(finalBufferPresentationTimeUs); && CompositingVideoSinkProvider.this.hasReleasedFrame(finalBufferPresentationTimeUs);
} }
@Override @Override
public void registerInputStream(@InputType int inputType, Format format) { public void registerInputStream(@InputType int inputType, Format format) {
checkState(isInitialized());
switch (inputType) { switch (inputType) {
case INPUT_TYPE_SURFACE: case INPUT_TYPE_SURFACE:
case INPUT_TYPE_BITMAP: case INPUT_TYPE_BITMAP:
@ -674,11 +675,13 @@ public final class CompositingVideoSinkProvider
@Override @Override
public Surface getInputSurface() { public Surface getInputSurface() {
return videoFrameProcessor.getInputSurface(); checkState(isInitialized());
return checkStateNotNull(videoFrameProcessor).getInputSurface();
} }
@Override @Override
public long registerInputFrame(long framePresentationTimeUs, boolean isLastFrame) { public long registerInputFrame(long framePresentationTimeUs, boolean isLastFrame) {
checkState(isInitialized());
checkState(videoFrameProcessorMaxPendingFrameCount != C.LENGTH_UNSET); checkState(videoFrameProcessorMaxPendingFrameCount != C.LENGTH_UNSET);
// An input stream is fully decoded, wait until all of its frames are released before queueing // An input stream is fully decoded, wait until all of its frames are released before queueing
@ -693,11 +696,11 @@ public final class CompositingVideoSinkProvider
} }
} }
if (videoFrameProcessor.getPendingInputFrameCount() if (checkStateNotNull(videoFrameProcessor).getPendingInputFrameCount()
>= videoFrameProcessorMaxPendingFrameCount) { >= videoFrameProcessorMaxPendingFrameCount) {
return C.TIME_UNSET; return C.TIME_UNSET;
} }
if (!videoFrameProcessor.registerInputFrame()) { if (!checkStateNotNull(videoFrameProcessor).registerInputFrame()) {
return C.TIME_UNSET; return C.TIME_UNSET;
} }
// The sink takes in frames with monotonically increasing, non-offset frame // The sink takes in frames with monotonically increasing, non-offset frame
@ -717,6 +720,8 @@ public final class CompositingVideoSinkProvider
@Override @Override
public boolean queueBitmap(Bitmap inputBitmap, TimestampIterator timestampIterator) { public boolean queueBitmap(Bitmap inputBitmap, TimestampIterator timestampIterator) {
checkState(isInitialized());
if (!maybeRegisterPendingInputStream()) { if (!maybeRegisterPendingInputStream()) {
return false; return false;
} }
@ -744,6 +749,7 @@ public final class CompositingVideoSinkProvider
@Override @Override
public void render(long positionUs, long elapsedRealtimeUs) throws VideoSinkException { public void render(long positionUs, long elapsedRealtimeUs) throws VideoSinkException {
checkState(isInitialized());
try { try {
CompositingVideoSinkProvider.this.render(positionUs, elapsedRealtimeUs); CompositingVideoSinkProvider.this.render(positionUs, elapsedRealtimeUs);
} catch (ExoPlaybackException e) { } catch (ExoPlaybackException e) {
@ -820,15 +826,16 @@ public final class CompositingVideoSinkProvider
} }
effects.addAll(videoEffects); effects.addAll(videoEffects);
Format inputFormat = checkNotNull(this.inputFormat); Format inputFormat = checkNotNull(this.inputFormat);
videoFrameProcessor.registerInputStream( checkStateNotNull(videoFrameProcessor)
inputType, .registerInputStream(
effects, inputType,
new FrameInfo.Builder( effects,
getAdjustedInputColorInfo(inputFormat.colorInfo), new FrameInfo.Builder(
inputFormat.width, getAdjustedInputColorInfo(inputFormat.colorInfo),
inputFormat.height) inputFormat.width,
.setPixelWidthHeightRatio(inputFormat.pixelWidthHeightRatio) inputFormat.height)
.build()); .setPixelWidthHeightRatio(inputFormat.pixelWidthHeightRatio)
.build());
finalBufferPresentationTimeUs = C.TIME_UNSET; finalBufferPresentationTimeUs = C.TIME_UNSET;
} }

View File

@ -22,6 +22,7 @@ import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.exoplayer.DecoderReuseEvaluation.DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED; import static androidx.media3.exoplayer.DecoderReuseEvaluation.DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED;
import static androidx.media3.exoplayer.DecoderReuseEvaluation.DISCARD_REASON_VIDEO_MAX_RESOLUTION_EXCEEDED; import static androidx.media3.exoplayer.DecoderReuseEvaluation.DISCARD_REASON_VIDEO_MAX_RESOLUTION_EXCEEDED;
import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_NO; import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_NO;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
@ -77,7 +78,6 @@ import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil.DecoderQueryException; import androidx.media3.exoplayer.mediacodec.MediaCodecUtil.DecoderQueryException;
import androidx.media3.exoplayer.video.VideoRendererEventListener.EventDispatcher; import androidx.media3.exoplayer.video.VideoRendererEventListener.EventDispatcher;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.MoreExecutors;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
import org.checkerframework.checker.initialization.qual.Initialized; import org.checkerframework.checker.initialization.qual.Initialized;
@ -367,12 +367,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
* @param assumedMinimumCodecOperatingRate A codec operating rate that all codecs instantiated by * @param assumedMinimumCodecOperatingRate A codec operating rate that all codecs instantiated by
* this renderer are assumed to meet implicitly (i.e. without the operating rate being set * this renderer are assumed to meet implicitly (i.e. without the operating rate being set
* explicitly using {@link MediaFormat#KEY_OPERATING_RATE}). * explicitly using {@link MediaFormat#KEY_OPERATING_RATE}).
* @param videoSinkProvider The {@link VideoSinkProvider} that will used be used for applying * @param videoSinkProvider The {@link VideoSinkProvider} that will be used for applying video
* video effects also providing the {@linkplain * effects also providing the {@linkplain VideoSinkProvider#getVideoFrameReleaseControl()
* VideoSinkProvider#getVideoFrameReleaseControl() VideoFrameReleaseControl} for releasing * VideoFrameReleaseControl} for releasing video frames. If {@code null}, the {@link
* video frames. If {@code null}, the {@link CompositingVideoSinkProvider} with its default * CompositingVideoSinkProvider} with its default configuration will be used, and the renderer
* configuration will be used, and the renderer will drive releasing of video frames by * will drive releasing of video frames by itself.
* itself.
*/ */
public MediaCodecVideoRenderer( public MediaCodecVideoRenderer(
Context context, Context context,
@ -733,7 +732,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
@Override @Override
protected void onRelease() { protected void onRelease() {
super.onRelease(); super.onRelease();
if (ownsVideoSinkProvider && videoSinkProvider.isInitialized()) { if (ownsVideoSinkProvider && videoSink != null) {
videoSinkProvider.release(); videoSinkProvider.release();
} }
} }
@ -777,7 +776,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
outputResolution = (Size) checkNotNull(message); outputResolution = (Size) checkNotNull(message);
// TODO: b/292111083 Set the surface on the videoSinkProvider before it's initialized // TODO: b/292111083 Set the surface on the videoSinkProvider before it's initialized
// otherwise the first frames are missed until a new video output resolution arrives. // otherwise the first frames are missed until a new video output resolution arrives.
if (videoSinkProvider.isInitialized() if (videoSink != null
&& checkNotNull(outputResolution).getWidth() != 0 && checkNotNull(outputResolution).getWidth() != 0
&& checkNotNull(outputResolution).getHeight() != 0 && checkNotNull(outputResolution).getHeight() != 0
&& displaySurface != null) { && displaySurface != null) {
@ -820,10 +819,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
@State int state = getState(); @State int state = getState();
@Nullable MediaCodecAdapter codec = getCodec(); @Nullable MediaCodecAdapter codec = getCodec();
// The video sink provider is initialized just before the first codec is ever created, so // The video sink is instantiated just before the first codec is ever created, so the sink can
// the provider can be initialized only when the codec is non-null. Therefore, we don't have // be non-null only when the codec is non-null. Therefore, we don't have to check if the sink
// to check if the provider is initialized but the codec is not. // is non-null but the codec is null.
if (codec != null && !videoSinkProvider.isInitialized()) { if (codec != null && videoSink == null) {
if (Util.SDK_INT >= 23 && displaySurface != null && !codecNeedsSetOutputSurfaceWorkaround) { if (Util.SDK_INT >= 23 && displaySurface != null && !codecNeedsSetOutputSurfaceWorkaround) {
setOutputSurfaceV23(codec, displaySurface); setOutputSurfaceV23(codec, displaySurface);
} else { } else {
@ -842,13 +841,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
videoFrameReleaseControl.join(/* renderNextFrameImmediately= */ true); videoFrameReleaseControl.join(/* renderNextFrameImmediately= */ true);
} }
// When effects previewing is enabled, set display surface and an unknown size. // When effects previewing is enabled, set display surface and an unknown size.
if (videoSinkProvider.isInitialized()) { if (videoSink != null) {
videoSinkProvider.setOutputSurfaceInfo(displaySurface, Size.UNKNOWN); videoSinkProvider.setOutputSurfaceInfo(displaySurface, Size.UNKNOWN);
} }
} else { } else {
// The display surface has been removed. // The display surface has been removed.
reportedVideoSize = null; reportedVideoSize = null;
if (videoSinkProvider.isInitialized()) { if (videoSink != null) {
videoSinkProvider.clearOutputSurfaceInfo(); videoSinkProvider.clearOutputSurfaceInfo();
} }
} }
@ -1059,59 +1058,72 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
@CallSuper @CallSuper
@Override @Override
protected void onReadyToInitializeCodec(Format format) throws ExoPlaybackException { protected void onReadyToInitializeCodec(Format format) throws ExoPlaybackException {
// We only enable effects preview on the first time a codec is initialized and if effects are // If the video sink provider is created by the renderer, we only enable effects preview (by
// using the video sink) on the first time a codec is initialized and if effects are
// already set. We do not enable effects mid-playback. For effects to be enabled after // already set. We do not enable effects mid-playback. For effects to be enabled after
// playback has started, the renderer needs to be reset first. // playback has started, the renderer needs to be reset first.
if (hasEffects && !hasInitializedPlayback && !videoSinkProvider.isInitialized()) { boolean enableEffectsForOwnSinkProvider =
ownsVideoSinkProvider && hasEffects && !hasInitializedPlayback;
// We always use the video sink if the video sink provider is passed to the renderer.
boolean useVideoSink = enableEffectsForOwnSinkProvider || !ownsVideoSinkProvider;
if (useVideoSink && videoSink == null) {
try { try {
videoSinkProvider.initialize(format); videoSinkProvider.setStreamOffsetUs(getOutputStreamOffsetUs());
if (displaySurface != null && outputResolution != null) { videoSink = videoSinkProvider.getSink();
videoSinkProvider.setOutputSurfaceInfo(displaySurface, outputResolution); if (!videoSink.isInitialized()) {
try {
videoSink.initialize(format);
} catch (VideoSink.VideoSinkException e) {
throw createRendererException(
e, format, PlaybackException.ERROR_CODE_VIDEO_FRAME_PROCESSOR_INIT_FAILED);
}
} }
} catch (VideoSink.VideoSinkException e) { videoSink.setListener(
throw createRendererException( new VideoSink.Listener() {
e, format, PlaybackException.ERROR_CODE_VIDEO_FRAME_PROCESSOR_INIT_FAILED); @Override
public void onFirstFrameRendered(VideoSink videoSink) {
checkStateNotNull(displaySurface);
notifyRenderedFirstFrame();
}
@Override
public void onFrameDropped(VideoSink videoSink) {
updateDroppedBufferCounters(
/* droppedInputBufferCount= */ 0, /* droppedDecoderBufferCount= */ 1);
}
@Override
public void onVideoSizeChanged(VideoSink videoSink, VideoSize videoSize) {
// TODO: b/292111083 - Report video size change to app. Video size reporting is
// removed at the moment to ensure the first frame is rendered, and the video is
// rendered after switching on/off the screen.
}
@Override
public void onError(
VideoSink videoSink, VideoSink.VideoSinkException videoSinkException) {
setPendingPlaybackException(
createRendererException(
videoSinkException,
videoSinkException.format,
PlaybackException.ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED));
}
},
// Pass a direct executor since the callback handling involves posting on the app looper
// again, so there's no need to do two hops.
directExecutor());
if (enableEffectsForOwnSinkProvider) {
if (displaySurface != null && outputResolution != null) {
videoSinkProvider.setOutputSurfaceInfo(displaySurface, outputResolution);
}
}
} catch (Exception e) {
// Set videoSink back to null so that, if the try block fails and the renderer retries the
// codec initialization, the try block is re-executed.
videoSink = null;
throw e;
} }
} }
if (videoSink == null && videoSinkProvider.isInitialized()) {
videoSinkProvider.setStreamOffsetUs(getOutputStreamOffsetUs());
videoSink = videoSinkProvider.getSink();
videoSink.setListener(
new VideoSink.Listener() {
@Override
public void onFirstFrameRendered(VideoSink videoSink) {
checkStateNotNull(displaySurface);
notifyRenderedFirstFrame();
}
@Override
public void onFrameDropped(VideoSink videoSink) {
updateDroppedBufferCounters(
/* droppedInputBufferCount= */ 0, /* droppedDecoderBufferCount= */ 1);
}
@Override
public void onVideoSizeChanged(VideoSink videoSink, VideoSize videoSize) {
// TODO: b/292111083 - Report video size change to app. Video size reporting is
// removed at the moment to ensure the first frame is rendered, and the video is
// rendered after switching on/off the screen.
}
@Override
public void onError(
VideoSink videoSink, VideoSink.VideoSinkException videoSinkException) {
setPendingPlaybackException(
createRendererException(
videoSinkException,
videoSinkException.format,
PlaybackException.ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED));
}
},
// Pass a direct executor since the callback handling involves posting on the app looper
// again, so there's no need to do two hops.
MoreExecutors.directExecutor());
}
hasInitializedPlayback = true; hasInitializedPlayback = true;
} }
@ -1335,7 +1347,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
// We are not rendering on a surface, the renderer will wait until a surface is set. // We are not rendering on a surface, the renderer will wait until a surface is set.
// Opportunistically render to VideoFrameProcessor if effects are enabled. // Opportunistically render to VideoFrameProcessor if effects are enabled.
if (displaySurface == placeholderSurface && !videoSinkProvider.isInitialized()) { if (displaySurface == placeholderSurface && videoSink == null) {
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes. // Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
if (videoFrameReleaseInfo.getEarlyUs() < 30_000) { if (videoFrameReleaseInfo.getEarlyUs() < 30_000) {
skipOutputBuffer(codec, bufferIndex, presentationTimeUs); skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
@ -1464,7 +1476,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
super.onProcessedStreamChange(); super.onProcessedStreamChange();
videoFrameReleaseControl.onProcessedStreamChange(); videoFrameReleaseControl.onProcessedStreamChange();
maybeSetupTunnelingForFirstFrame(); maybeSetupTunnelingForFirstFrame();
if (videoSinkProvider.isInitialized()) { if (videoSink != null) {
videoSinkProvider.setStreamOffsetUs(getOutputStreamOffsetUs()); videoSinkProvider.setStreamOffsetUs(getOutputStreamOffsetUs());
} }
} }

View File

@ -108,6 +108,17 @@ public interface VideoSink {
*/ */
void setListener(Listener listener, Executor executor); void setListener(Listener listener, Executor executor);
/**
* Initializes the video sink.
*
* @param sourceFormat The format of the compressed video.
* @throws VideoSink.VideoSinkException If initializing the sink failed.
*/
void initialize(Format sourceFormat) throws VideoSinkException;
/** Returns whether the video sink is {@linkplain #initialize(Format) initialized}. */
boolean isInitialized();
/** /**
* Flushes the video sink. * Flushes the video sink.
* *
@ -115,20 +126,28 @@ public interface VideoSink {
*/ */
void flush(); void flush();
/** Whether the video sink is able to immediately render media from the current position. */ /**
* Returns whether the video sink is able to immediately render media from the current position.
*/
boolean isReady(); boolean isReady();
/** /**
* Whether all queued video frames have been rendered, including the frame marked as last buffer. * Returns whether all queued video frames have been rendered, including the frame marked as last
* buffer.
*/ */
boolean isEnded(); boolean isEnded();
/** /**
* Whether frames could be dropped from the sink's {@linkplain #getInputSurface() input surface}. * Returns whether frames could be dropped from the sink's {@linkplain #getInputSurface() input
* surface}.
*/ */
boolean isFrameDropAllowedOnInput(); boolean isFrameDropAllowedOnInput();
/** Returns the input {@link Surface} where the video sink consumes input frames from. */ /**
* Returns the input {@link Surface} where the video sink consumes input frames from.
*
* <p>Must be called after the sink is {@linkplain #initialize(Format) initialized}.
*/
Surface getInputSurface(); Surface getInputSurface();
/** Sets the playback speed. */ /** Sets the playback speed. */
@ -137,6 +156,8 @@ public interface VideoSink {
/** /**
* Informs the video sink that a new input stream will be queued. * Informs the video sink that a new input stream will be queued.
* *
* <p>Must be called after the sink is {@linkplain #initialize(Format) initialized}.
*
* @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.
*/ */
@ -146,9 +167,11 @@ public interface VideoSink {
* Informs the video sink that a frame will be queued to its {@linkplain #getInputSurface() input * Informs the video sink that a frame will be queued to its {@linkplain #getInputSurface() input
* surface}. * surface}.
* *
* <p>Must be called after the sink is {@linkplain #initialize(Format) initialized}.
*
* @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. * @param isLastFrame Whether this is the last frame of the video stream.
* @return a release timestamp, in nanoseconds, that should be associated when releasing this * @return A release timestamp, in nanoseconds, that should be associated when releasing this
* frame, or {@link C#TIME_UNSET} if the sink was not able to register the frame and the * frame, or {@link C#TIME_UNSET} if the sink was not able to register the frame and the
* caller must try again later. * caller must try again later.
*/ */
@ -157,6 +180,8 @@ public interface VideoSink {
/** /**
* Provides an input {@link Bitmap} to the video sink. * Provides an input {@link Bitmap} to the video sink.
* *
* <p>Must be called after the sink is {@linkplain #initialize(Format) initialized}.
*
* @param inputBitmap The {@link Bitmap} queued to the video sink. * @param inputBitmap The {@link Bitmap} queued 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
* at. The timestamps should be monotonically increasing. * at. The timestamps should be monotonically increasing.
@ -168,6 +193,8 @@ public interface VideoSink {
/** /**
* Incrementally renders processed video frames. * Incrementally renders processed video frames.
* *
* <p>Must be called after the sink is {@linkplain #initialize(Format) initialized}.
*
* @param positionUs The current playback position, in microseconds. * @param positionUs The current playback position, in microseconds.
* @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
* taken approximately at the time the playback position was {@code positionUs}. * taken approximately at the time the playback position was {@code positionUs}.

View File

@ -33,7 +33,8 @@ public interface VideoSinkProvider {
* Sets the {@link VideoFrameReleaseControl} that will be used for releasing of video frames * Sets the {@link VideoFrameReleaseControl} that will be used for releasing of video frames
* during rendering. * during rendering.
* *
* <p>Must be called before the sink provider is {@linkplain #initialize(Format) initialized}. * <p>Must be called before the first {@linkplain #getSink() sink} is {@linkplain
* VideoSink#initialize(Format) initialized}.
*/ */
void setVideoFrameReleaseControl(VideoFrameReleaseControl videoFrameReleaseControl); void setVideoFrameReleaseControl(VideoFrameReleaseControl videoFrameReleaseControl);
@ -49,7 +50,8 @@ public interface VideoSinkProvider {
/** /**
* Sets the {@link Clock} that the provider should use internally. * Sets the {@link Clock} that the provider should use internally.
* *
* <p>Must be called before the sink provider is {@linkplain #initialize(Format) initialized}. * <p>Must be called before the first {@linkplain #getSink() sink} is {@linkplain
* VideoSink#initialize(Format) initialized}.
*/ */
void setClock(Clock clock); void setClock(Clock clock);
@ -62,29 +64,12 @@ public interface VideoSinkProvider {
*/ */
void setPendingVideoEffects(List<Effect> videoEffects); void setPendingVideoEffects(List<Effect> videoEffects);
/** /** Returns a {@link VideoSink} to forward video frames for processing. */
* Initializes the provider for video frame processing. Can be called up to one time.
*
* @param sourceFormat The format of the compressed video.
* @throws VideoSink.VideoSinkException If enabling the provider failed.
*/
void initialize(Format sourceFormat) throws VideoSink.VideoSinkException;
/** Returns whether this provider is initialized for frame processing. */
boolean isInitialized();
/**
* Returns a {@link VideoSink} to forward video frames for processing.
*
* <p>Must be called after the sink provider is {@linkplain #initialize(Format) initialized}.
*/
VideoSink getSink(); VideoSink getSink();
/** /**
* Sets the offset, in microseconds, that is added to the video frames presentation timestamps * Sets the offset, in microseconds, that is added to the video frames presentation timestamps
* from the player. * from the player.
*
* <p>Must be called after the sink provider is {@linkplain #initialize(Format) initialized}.
*/ */
void setStreamOffsetUs(long streamOffsetUs); void setStreamOffsetUs(long streamOffsetUs);

View File

@ -51,50 +51,33 @@ public final class CompositingVideoSinkProviderTest {
} }
@Test @Test
public void initialize() throws VideoSink.VideoSinkException { public void initializeSink_withoutReleaseControl_throws() {
CompositingVideoSinkProvider provider = createCompositingVideoSinkProvider(); CompositingVideoSinkProvider provider =
new CompositingVideoSinkProvider.Builder(ApplicationProvider.getApplicationContext())
provider.initialize(new Format.Builder().build()); .setPreviewingVideoGraphFactory(new TestPreviewingVideoGraphFactory())
.build();
assertThat(provider.isInitialized()).isTrue(); VideoSink sink = provider.getSink();
}
@Test
public void initialize_calledTwice_throws() throws VideoSink.VideoSinkException {
CompositingVideoSinkProvider provider = createCompositingVideoSinkProvider();
provider.initialize(new Format.Builder().build());
assertThrows( assertThrows(
IllegalStateException.class, () -> provider.initialize(new Format.Builder().build())); IllegalStateException.class,
() -> sink.initialize(new Format.Builder().setWidth(640).setHeight(480).build()));
} }
@Test @Test
public void isInitialized_afterRelease_returnsFalse() throws VideoSink.VideoSinkException { public void initializeSink_calledTwice_throws() throws VideoSink.VideoSinkException {
CompositingVideoSinkProvider provider = createCompositingVideoSinkProvider(); CompositingVideoSinkProvider provider = createCompositingVideoSinkProvider();
provider.initialize(new Format.Builder().build()); VideoSink sink = provider.getSink();
sink.initialize(new Format.Builder().build());
provider.release(); assertThrows(IllegalStateException.class, () -> sink.initialize(new Format.Builder().build()));
assertThat(provider.isInitialized()).isFalse();
}
@Test
public void initialize_afterRelease_throws() throws VideoSink.VideoSinkException {
CompositingVideoSinkProvider provider = createCompositingVideoSinkProvider();
Format format = new Format.Builder().build();
provider.initialize(format);
provider.release();
assertThrows(IllegalStateException.class, () -> provider.initialize(format));
} }
@Test @Test
public void setOutputStreamOffsetUs_frameReleaseTimesAreAdjusted() public void setOutputStreamOffsetUs_frameReleaseTimesAreAdjusted()
throws VideoSink.VideoSinkException { throws VideoSink.VideoSinkException {
CompositingVideoSinkProvider provider = createCompositingVideoSinkProvider(); CompositingVideoSinkProvider provider = createCompositingVideoSinkProvider();
provider.initialize(new Format.Builder().build());
VideoSink videoSink = provider.getSink(); VideoSink videoSink = provider.getSink();
videoSink.initialize(new Format.Builder().build());
videoSink.registerInputStream( videoSink.registerInputStream(
VideoSink.INPUT_TYPE_SURFACE, new Format.Builder().setWidth(640).setHeight(480).build()); VideoSink.INPUT_TYPE_SURFACE, new Format.Builder().setWidth(640).setHeight(480).build());