Remove setPendingVideoEffects from VideoSink.

VideoSink#onInputStreamChanged(int, Format, List<Effect>) should now be used to set video effects on a new input stream.

PiperOrigin-RevId: 713627389
This commit is contained in:
Googler 2025-01-09 04:45:21 -08:00 committed by Copybara-Service
parent e1b57c130d
commit 8a709a7d76
7 changed files with 154 additions and 65 deletions

View File

@ -15,6 +15,7 @@
*/
package androidx.media3.exoplayer.video;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.graphics.Bitmap;
@ -170,16 +171,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*
* <p>This method will always throw an {@link UnsupportedOperationException}.
*/
@Override
public void setPendingVideoEffects(List<Effect> videoEffects) {
throw new UnsupportedOperationException();
}
@Override
public void setStreamTimestampInfo(
long streamStartPositionUs, long bufferTimestampAdjustmentUs, long lastResetPositionUs) {
@ -212,8 +203,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
videoFrameReleaseControl.allowReleaseFirstFrameBeforeStarted();
}
/**
* {@inheritDoc}
*
* <p>{@code videoEffects} is required to be empty
*/
@Override
public void onInputStreamChanged(@InputType int inputType, Format format) {
public void onInputStreamChanged(
@InputType int inputType, Format format, List<Effect> videoEffects) {
checkState(videoEffects.isEmpty());
if (format.width != inputFormat.width || format.height != inputFormat.height) {
videoFrameRenderControl.onVideoSizeChanged(format.width, format.height);
}

View File

@ -1397,8 +1397,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
decodedVideoSize = new VideoSize(width, height, pixelWidthHeightRatio);
if (videoSink != null && pendingVideoSinkInputStreamChange) {
onReadyToChangeVideoSinkInputStream();
videoSink.onInputStreamChanged(
changeVideoSinkInputStream(
videoSink,
/* inputType= */ VideoSink.INPUT_TYPE_SURFACE,
format
.buildUpon()
@ -1413,13 +1413,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
}
/**
* Called when ready to {@linkplain VideoSink#onInputStreamChanged(int, Format) change} the input
* stream when {@linkplain #setVideoEffects video effects} are enabled.
* Called when ready to {@linkplain VideoSink#onInputStreamChanged(int, Format, List<Effect>)
* change} the input stream.
*
* <p>The default implementation is a no-op.
* <p>The default implementation applies this renderer's video effects.
*/
protected void onReadyToChangeVideoSinkInputStream() {
// do nothing.
protected void changeVideoSinkInputStream(
VideoSink videoSink, @VideoSink.InputType int inputType, Format format) {
List<Effect> videoEffectsToApply = videoEffects != null ? videoEffects : ImmutableList.of();
videoSink.onInputStreamChanged(inputType, format, videoEffectsToApply);
}
@Override

View File

@ -382,13 +382,15 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
// We forward output size changes to the sink even if we are still flushing.
videoGraphOutputFormat =
videoGraphOutputFormat.buildUpon().setWidth(width).setHeight(height).build();
defaultVideoSink.onInputStreamChanged(INPUT_TYPE_SURFACE, videoGraphOutputFormat);
defaultVideoSink.onInputStreamChanged(
INPUT_TYPE_SURFACE, videoGraphOutputFormat, /* videoEffects= */ ImmutableList.of());
}
@Override
public void onOutputFrameRateChanged(float frameRate) {
videoGraphOutputFormat = videoGraphOutputFormat.buildUpon().setFrameRate(frameRate).build();
defaultVideoSink.onInputStreamChanged(INPUT_TYPE_SURFACE, videoGraphOutputFormat);
defaultVideoSink.onInputStreamChanged(
INPUT_TYPE_SURFACE, videoGraphOutputFormat, /* videoEffects= */ ImmutableList.of());
}
@Override
@ -685,7 +687,8 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
}
@Override
public void onInputStreamChanged(@InputType int inputType, Format format) {
public void onInputStreamChanged(
@InputType int inputType, Format format, List<Effect> videoEffects) {
checkState(isInitialized());
switch (inputType) {
case INPUT_TYPE_SURFACE:
@ -694,6 +697,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
default:
throw new UnsupportedOperationException("Unsupported input type " + inputType);
}
setPendingVideoEffects(videoEffects);
this.inputType = inputType;
this.inputFormat = format;
finalBufferPresentationTimeUs = C.TIME_UNSET;
@ -729,15 +733,6 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
}
}
@Override
public void setPendingVideoEffects(List<Effect> videoEffects) {
this.videoEffects =
new ImmutableList.Builder<Effect>()
.addAll(videoEffects)
.addAll(compositionEffects)
.build();
}
@Override
public void setStreamTimestampInfo(
long streamStartPositionUs, long bufferTimestampAdjustmentUs, long lastResetPositionUs) {
@ -887,6 +882,19 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
// Private methods
/**
* Sets the pending video effects.
*
* <p>Effects are pending until a new input stream is registered.
*/
private void setPendingVideoEffects(List<Effect> newVideoEffects) {
this.videoEffects =
new ImmutableList.Builder<Effect>()
.addAll(newVideoEffects)
.addAll(compositionEffects)
.build();
}
private void registerInputStream(Format inputFormat) {
Format adjustedInputFormat =
inputFormat

View File

@ -187,8 +187,9 @@ public interface VideoSink {
*
* <p>This method returns {@code true} if the end of the last input stream has been {@linkplain
* #signalEndOfCurrentInputStream() signaled} and all the input frames have been rendered. Note
* that a new input stream can be {@linkplain #onInputStreamChanged(int, Format) signaled} even
* when this method returns true (in which case the sink will not be ended anymore).
* that a new input stream can be {@linkplain #onInputStreamChanged(int, Format, List<Effect>)
* signaled} even when this method returns true (in which case the sink will not be ended
* anymore).
*/
boolean isEnded();
@ -208,12 +209,6 @@ public interface VideoSink {
/** Sets {@linkplain Effect video effects} to apply immediately. */
void setVideoEffects(List<Effect> videoEffects);
/**
* Sets {@linkplain Effect video effects} to apply after the next stream {@linkplain
* VideoSink#onInputStreamChanged(int, Format) change}.
*/
void setPendingVideoEffects(List<Effect> videoEffects);
/**
* Sets information about the timestamps of the current input stream.
*
@ -250,20 +245,21 @@ public interface VideoSink {
void enableMayRenderStartOfStream();
/**
* Informs the video sink that a new input stream will be queued.
* Informs the video sink that a new input stream will be queued with the given effects.
*
* <p>Must be called after the sink is {@linkplain #initialize(Format) initialized}.
*
* @param inputType The {@link InputType} of the stream.
* @param format The {@link Format} of the stream.
* @param videoEffects The {@link List<Effect>} to apply to the new stream.
*/
void onInputStreamChanged(@InputType int inputType, Format format);
void onInputStreamChanged(@InputType int inputType, Format format, List<Effect> videoEffects);
/**
* Handles a video input frame.
*
* <p>Must be called after the corresponding stream is {@linkplain #onInputStreamChanged(int,
* Format) signaled}.
* Format, List<Effect>) signaled}.
*
* @param framePresentationTimeUs The frame's presentation time, in microseconds.
* @param isLastFrame Whether this is the last frame of the video stream. This flag is set on a
@ -280,7 +276,7 @@ public interface VideoSink {
* Handles an input {@link Bitmap}.
*
* <p>Must be called after the corresponding stream is {@linkplain #onInputStreamChanged(int,
* Format) signaled}.
* Format, List<Effect>) signaled}.
*
* @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

View File

@ -15,20 +15,28 @@
*/
package androidx.media3.exoplayer.video;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.graphics.Bitmap;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.media3.common.ColorInfo;
import androidx.media3.common.DebugViewProvider;
import androidx.media3.common.Effect;
import androidx.media3.common.Format;
import androidx.media3.common.OnInputFrameProcessedListener;
import androidx.media3.common.PreviewingVideoGraph;
import androidx.media3.common.SurfaceInfo;
import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.VideoGraph;
import androidx.media3.common.util.TimestampIterator;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.concurrent.Executor;
import org.junit.Test;
@ -51,17 +59,40 @@ public final class PlaybackVideoGraphWrapperTest {
@Test
public void initializeSink_calledTwice_throws() throws VideoSink.VideoSinkException {
PlaybackVideoGraphWrapper provider = createPlaybackVideoGraphWrapper();
VideoSink sink = provider.getSink();
PlaybackVideoGraphWrapper playbackVideoGraphWrapper =
createPlaybackVideoGraphWrapper(new FakeVideoFrameProcessor());
VideoSink sink = playbackVideoGraphWrapper.getSink();
sink.initialize(new Format.Builder().build());
assertThrows(IllegalStateException.class, () -> sink.initialize(new Format.Builder().build()));
}
private static PlaybackVideoGraphWrapper createPlaybackVideoGraphWrapper() {
@Test
public void onInputStreamChanged_setsVideoSinkVideoEffects() throws VideoSink.VideoSinkException {
ImmutableList<Effect> firstEffects = ImmutableList.of(Mockito.mock(Effect.class));
ImmutableList<Effect> secondEffects =
ImmutableList.of(Mockito.mock(Effect.class), Mockito.mock(Effect.class));
FakeVideoFrameProcessor videoFrameProcessor = new FakeVideoFrameProcessor();
PlaybackVideoGraphWrapper playbackVideoGraphWrapper =
createPlaybackVideoGraphWrapper(videoFrameProcessor);
Format format = new Format.Builder().build();
VideoSink sink = playbackVideoGraphWrapper.getSink();
sink.initialize(format);
sink.onInputStreamChanged(VideoSink.INPUT_TYPE_SURFACE, format, firstEffects);
assertThat(videoFrameProcessor.registeredEffects).isEqualTo(firstEffects);
sink.onInputStreamChanged(VideoSink.INPUT_TYPE_SURFACE, format, secondEffects);
assertThat(videoFrameProcessor.registeredEffects).isEqualTo(secondEffects);
sink.onInputStreamChanged(VideoSink.INPUT_TYPE_SURFACE, format, ImmutableList.of());
assertThat(videoFrameProcessor.registeredEffects).isEmpty();
}
private static PlaybackVideoGraphWrapper createPlaybackVideoGraphWrapper(
VideoFrameProcessor videoFrameProcessor) {
Context context = ApplicationProvider.getApplicationContext();
return new PlaybackVideoGraphWrapper.Builder(context, createVideoFrameReleaseControl())
.setPreviewingVideoGraphFactory(new TestPreviewingVideoGraphFactory())
.setPreviewingVideoGraphFactory(new TestPreviewingVideoGraphFactory(videoFrameProcessor))
.build();
}
@ -94,12 +125,73 @@ public final class PlaybackVideoGraphWrapperTest {
context, frameTimingEvaluator, /* allowedJoiningTimeMs= */ 0);
}
private static class FakeVideoFrameProcessor implements VideoFrameProcessor {
List<Effect> registeredEffects = ImmutableList.of();
@Override
public boolean queueInputBitmap(Bitmap inputBitmap, TimestampIterator timestampIterator) {
return false;
}
@Override
public boolean queueInputTexture(int textureId, long presentationTimeUs) {
return false;
}
@Override
public void setOnInputFrameProcessedListener(OnInputFrameProcessedListener listener) {}
@Override
public void setOnInputSurfaceReadyListener(Runnable listener) {}
@Override
public Surface getInputSurface() {
return null;
}
@Override
public void registerInputStream(
@InputType int inputType, Format format, List<Effect> effects, long offsetToAddUs) {
registeredEffects = effects;
}
@Override
public boolean registerInputFrame() {
return true;
}
@Override
public int getPendingInputFrameCount() {
return 0;
}
@Override
public void setOutputSurfaceInfo(@Nullable SurfaceInfo outputSurfaceInfo) {}
@Override
public void renderOutputFrame(long renderTimeNs) {}
@Override
public void signalEndOfInput() {}
@Override
public void flush() {}
@Override
public void release() {}
}
private static class TestPreviewingVideoGraphFactory implements PreviewingVideoGraph.Factory {
// Using a mock but we don't assert mock interactions. If needed to assert interactions, we
// should a fake instead.
private final PreviewingVideoGraph previewingVideoGraph =
Mockito.mock(PreviewingVideoGraph.class);
private final VideoFrameProcessor videoFrameProcessor = Mockito.mock(VideoFrameProcessor.class);
private final VideoFrameProcessor videoFrameProcessor;
public TestPreviewingVideoGraphFactory(VideoFrameProcessor videoFrameProcessor) {
this.videoFrameProcessor = videoFrameProcessor;
}
@Override
public PreviewingVideoGraph create(
@ -111,7 +203,6 @@ public final class PlaybackVideoGraphWrapperTest {
List<Effect> compositionEffects,
long initialTimestampOffsetUs) {
when(previewingVideoGraph.getProcessor(anyInt())).thenReturn(videoFrameProcessor);
when(videoFrameProcessor.registerInputFrame()).thenReturn(true);
return previewingVideoGraph;
}
}

View File

@ -176,11 +176,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
executeOrDelay(videoSink -> videoSink.setVideoEffects(videoEffects));
}
@Override
public void setPendingVideoEffects(List<Effect> videoEffects) {
executeOrDelay(videoSink -> videoSink.setPendingVideoEffects(videoEffects));
}
@Override
public void setStreamTimestampInfo(
long streamStartPositionUs, long bufferTimestampAdjustmentUs, long lastResetPositionUs) {
@ -211,8 +206,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
@Override
public void onInputStreamChanged(@InputType int inputType, Format format) {
executeOrDelay(videoSink -> videoSink.onInputStreamChanged(inputType, format));
public void onInputStreamChanged(
@InputType int inputType, Format format, List<Effect> videoEffects) {
executeOrDelay(videoSink -> videoSink.onInputStreamChanged(inputType, format, videoEffects));
}
@Override

View File

@ -287,7 +287,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final VideoSink videoSink;
private final boolean requestToneMapping;
@Nullable private ImmutableList<Effect> pendingEffect;
private ImmutableList<Effect> pendingEffects;
@Nullable private EditedMediaItem currentEditedMediaItem;
private long offsetToCompositionTimeUs;
@ -312,6 +312,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.sequence = sequence;
this.videoSink = videoSink;
this.requestToneMapping = requestToneMapping;
this.pendingEffects = ImmutableList.of();
experimentalEnableProcessedStreamChangedAtStart();
}
@ -329,7 +330,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// previous one.
currentEditedMediaItem = getRepeatedEditedMediaItem(sequence, mediaItemIndex);
offsetToCompositionTimeUs = getOffsetToCompositionTimeUs(sequence, mediaItemIndex, offsetUs);
pendingEffect = sequence.editedMediaItems.get(mediaItemIndex).effects.videoEffects;
pendingEffects = sequence.editedMediaItems.get(mediaItemIndex).effects.videoEffects;
super.onStreamChanged(formats, startPositionUs, offsetUs, mediaPeriodId);
}
@ -370,12 +371,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
@Override
protected void onReadyToChangeVideoSinkInputStream() {
@Nullable ImmutableList<Effect> pendingEffect = this.pendingEffect;
if (pendingEffect != null) {
videoSink.setPendingVideoEffects(pendingEffect);
this.pendingEffect = null;
}
protected void changeVideoSinkInputStream(
VideoSink videoSink, @VideoSink.InputType int inputType, Format format) {
videoSink.onInputStreamChanged(inputType, format, pendingEffects);
}
}
@ -519,7 +517,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
long positionUs, long elapsedRealtimeUs, Bitmap outputImage, long timeUs) {
if (inputStreamPending) {
checkState(streamStartPositionUs != C.TIME_UNSET);
videoSink.setPendingVideoEffects(videoEffects);
videoSink.setStreamTimestampInfo(
streamStartPositionUs,
/* bufferTimestampAdjustmentUs= */ offsetToCompositionTimeUs,
@ -532,7 +529,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
.setHeight(outputImage.getHeight())
.setColorInfo(ColorInfo.SRGB_BT709_FULL)
.setFrameRate(/* frameRate= */ DEFAULT_FRAME_RATE)
.build());
.build(),
videoEffects);
inputStreamPending = false;
}
if (!videoSink.handleInputBitmap(outputImage, checkStateNotNull(timestampIterator))) {