Various improvements to BufferingVideoSink

PiperOrigin-RevId: 717807436
This commit is contained in:
kimvde 2025-01-21 01:52:31 -08:00 committed by Copybara-Service
parent 5421a74d06
commit 635e699965
2 changed files with 60 additions and 61 deletions

View File

@ -33,13 +33,16 @@ import java.util.concurrent.Executor;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* A {@link VideoSink} that delays the operations performed on it until it {@linkplain * A {@link VideoSink} that delays most operations performed on it until it {@linkplain
* #setVideoSink(VideoSink) receives} a sink. * #setVideoSink(VideoSink) receives} a sink.
*
* <p>Some operations are not delayed. Their behavior in case there is no underlying {@link
* VideoSink} is documented in the corresponding method.
*/ */
/* package */ final class BufferingVideoSink implements VideoSink { /* package */ final class BufferingVideoSink implements VideoSink {
private final Context context; private final Context context;
private final List<ThrowingVideoSinkOperation> pendingOperations; private final List<VideoSinkOperation> pendingOperations;
@Nullable private VideoSink videoSink; @Nullable private VideoSink videoSink;
private boolean isInitialized; private boolean isInitialized;
@ -53,24 +56,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* Sets the {@link VideoSink} to execute the pending and future operations on. * Sets the {@link VideoSink} to execute the pending and future operations on.
* *
* @param videoSink The {@link VideoSink} to execute the operations on. * @param videoSink The {@link VideoSink} to execute the operations on, or {@code null} to remove
* @throws VideoSinkException If an error occurred executing the pending operations on the sink. * the underlying {@link VideoSink}.
*/ */
public void setVideoSink(VideoSink videoSink) throws VideoSinkException { public void setVideoSink(@Nullable VideoSink videoSink) {
this.videoSink = videoSink; this.videoSink = videoSink;
if (videoSink == null) {
return;
}
for (int i = 0; i < pendingOperations.size(); i++) { for (int i = 0; i < pendingOperations.size(); i++) {
pendingOperations.get(i).execute(videoSink); pendingOperations.get(i).execute(videoSink);
} }
pendingOperations.clear(); pendingOperations.clear();
} }
/**
* Removes the underlying {@link VideoSink} if it is {@linkplain #setVideoSink(VideoSink) set}.
*/
public void removeVideoSink() {
this.videoSink = null;
}
/** Returns the underlying {@link VideoSink} or {@code null} if there is none. */ /** Returns the underlying {@link VideoSink} or {@code null} if there is none. */
@Nullable @Nullable
public VideoSink getVideoSink() { public VideoSink getVideoSink() {
@ -107,17 +106,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
executeOrDelay(videoSink -> videoSink.setListener(listener, executor)); executeOrDelay(videoSink -> videoSink.setListener(listener, executor));
} }
/**
* {@inheritDoc}
*
* <p>This operation won't be added to the pending operations if the {@linkplain
* #setVideoSink(VideoSink) underlying sink} is {@code null}.
*
* <p>{@code true} is always returned if the {@linkplain #setVideoSink(VideoSink) underlying sink}
* is {@code null}.
*/
@Override @Override
public boolean initialize(Format sourceFormat) throws VideoSinkException { public boolean initialize(Format sourceFormat) throws VideoSinkException {
executeOrDelayThrowing( isInitialized = videoSink == null || videoSink.initialize(sourceFormat);
videoSink -> { return isInitialized;
if (videoSink.isInitialized()) {
return;
}
videoSink.initialize(sourceFormat);
});
isInitialized = true;
return true;
} }
@Override @Override
@ -130,8 +131,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
executeOrDelay(videoSink -> videoSink.flush(resetPosition)); executeOrDelay(videoSink -> videoSink.flush(resetPosition));
} }
/**
* {@inheritDoc}
*
* <p>{@code true} is always returned if the {@linkplain #setVideoSink(VideoSink) underlying sink}
* is {@code null}.
*/
@Override @Override
public boolean isReady(boolean rendererOtherwiseReady) { public boolean isReady(boolean rendererOtherwiseReady) {
// Return true if the VideoSink is null to indicate that the renderer can be started. Indeed,
// for prewarming, a VideoSink is set on the BufferingVideoSink when the renderer is started.
return videoSink == null || videoSink.isReady(rendererOtherwiseReady); return videoSink == null || videoSink.isReady(rendererOtherwiseReady);
} }
@ -145,6 +154,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
executeOrDelay(VideoSink::signalEndOfInput); executeOrDelay(VideoSink::signalEndOfInput);
} }
/**
* {@inheritDoc}
*
* <p>{@code false} is always returned if the {@linkplain #setVideoSink(VideoSink) underlying
* sink} is {@code null}.
*/
@Override @Override
public boolean isEnded() { public boolean isEnded() {
return videoSink != null && videoSink.isEnded(); return videoSink != null && videoSink.isEnded();
@ -212,6 +227,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
executeOrDelay(videoSink -> videoSink.onInputStreamChanged(inputType, format, videoEffects)); executeOrDelay(videoSink -> videoSink.onInputStreamChanged(inputType, format, videoEffects));
} }
/**
* {@inheritDoc}
*
* <p>{@code false} is always returned if the {@linkplain #setVideoSink(VideoSink) underlying
* sink} is {@code null}.
*/
@Override @Override
public boolean handleInputFrame( public boolean handleInputFrame(
long framePresentationTimeUs, boolean isLastFrame, VideoFrameHandler videoFrameHandler) { long framePresentationTimeUs, boolean isLastFrame, VideoFrameHandler videoFrameHandler) {
@ -219,11 +240,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
&& videoSink.handleInputFrame(framePresentationTimeUs, isLastFrame, videoFrameHandler); && videoSink.handleInputFrame(framePresentationTimeUs, isLastFrame, videoFrameHandler);
} }
/**
* {@inheritDoc}
*
* <p>{@code false} is always returned if the {@linkplain #setVideoSink(VideoSink) underlying
* sink} is {@code null}.
*/
@Override @Override
public boolean handleInputBitmap(Bitmap inputBitmap, TimestampIterator timestampIterator) { public boolean handleInputBitmap(Bitmap inputBitmap, TimestampIterator timestampIterator) {
return videoSink != null && videoSink.handleInputBitmap(inputBitmap, timestampIterator); return videoSink != null && videoSink.handleInputBitmap(inputBitmap, timestampIterator);
} }
/**
* {@inheritDoc}
*
* <p>This operation won't be added to the pending operations if the {@linkplain
* #setVideoSink(VideoSink) underlying sink} is {@code null}.
*/
@Override @Override
public void render(long positionUs, long elapsedRealtimeUs) throws VideoSinkException { public void render(long positionUs, long elapsedRealtimeUs) throws VideoSinkException {
if (videoSink != null) { if (videoSink != null) {
@ -257,15 +290,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
private void executeOrDelayThrowing(ThrowingVideoSinkOperation operation)
throws VideoSinkException {
if (videoSink != null) {
operation.execute(videoSink);
} else {
pendingOperations.add(operation);
}
}
private PlaceholderSurface getPlaceholderSurface() { private PlaceholderSurface getPlaceholderSurface() {
if (placeholderSurface == null) { if (placeholderSurface == null) {
placeholderSurface = PlaceholderSurface.newInstance(context, /* secure= */ false); placeholderSurface = PlaceholderSurface.newInstance(context, /* secure= */ false);
@ -273,14 +297,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return placeholderSurface; return placeholderSurface;
} }
private interface ThrowingVideoSinkOperation { private interface VideoSinkOperation {
void execute(VideoSink videoSink) throws VideoSinkException;
}
private interface VideoSinkOperation extends ThrowingVideoSinkOperation {
@Override
void execute(VideoSink videoSink); void execute(VideoSink videoSink);
} }
} }

View File

@ -15,15 +15,11 @@
*/ */
package androidx.media3.transformer; package androidx.media3.transformer;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import android.content.Context; import android.content.Context;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.exoplayer.video.VideoSink; import androidx.media3.exoplayer.video.VideoSink;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -39,7 +35,7 @@ public class BufferingVideoSinkTest {
private final Context context = ApplicationProvider.getApplicationContext(); private final Context context = ApplicationProvider.getApplicationContext();
@Test @Test
public void executeOperation_withVideoSinkSet_callsVideoSink() throws Exception { public void executeOperation_withVideoSinkSet_callsVideoSink() {
BufferingVideoSink bufferingVideoSink = new BufferingVideoSink(context); BufferingVideoSink bufferingVideoSink = new BufferingVideoSink(context);
VideoSink videoSinkMock = mock(VideoSink.class); VideoSink videoSinkMock = mock(VideoSink.class);
@ -53,7 +49,7 @@ public class BufferingVideoSinkTest {
} }
@Test @Test
public void setVideoSink_executesPendingOperations() throws Exception { public void setVideoSink_executesPendingOperations() {
BufferingVideoSink bufferingVideoSink = new BufferingVideoSink(context); BufferingVideoSink bufferingVideoSink = new BufferingVideoSink(context);
VideoSink videoSinkMock = mock(VideoSink.class); VideoSink videoSinkMock = mock(VideoSink.class);
@ -67,27 +63,12 @@ public class BufferingVideoSinkTest {
} }
@Test @Test
public void setVideoSink_withFailingPendingOperation_throws() throws Exception { public void setNullVideoSink_thenExecuteOperations_doesNotCallVideoSink() {
BufferingVideoSink bufferingVideoSink = new BufferingVideoSink(context);
VideoSink videoSinkMock = mock(VideoSink.class);
Format format = new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build();
Mockito.doThrow(new VideoSink.VideoSinkException(new RuntimeException(), format))
.when(videoSinkMock)
.initialize(any());
bufferingVideoSink.initialize(format);
assertThrows(
VideoSink.VideoSinkException.class, () -> bufferingVideoSink.setVideoSink(videoSinkMock));
}
@Test
public void removeVideoSink_thenExecuteOperations_doesNotCallVideoSink() throws Exception {
BufferingVideoSink bufferingVideoSink = new BufferingVideoSink(context); BufferingVideoSink bufferingVideoSink = new BufferingVideoSink(context);
VideoSink videoSinkMock = mock(VideoSink.class); VideoSink videoSinkMock = mock(VideoSink.class);
bufferingVideoSink.setVideoSink(videoSinkMock); bufferingVideoSink.setVideoSink(videoSinkMock);
bufferingVideoSink.removeVideoSink(); bufferingVideoSink.setVideoSink(null);
bufferingVideoSink.onRendererEnabled(/* mayRenderStartOfStream= */ true); bufferingVideoSink.onRendererEnabled(/* mayRenderStartOfStream= */ true);
bufferingVideoSink.onRendererStarted(); bufferingVideoSink.onRendererStarted();
@ -96,7 +77,7 @@ public class BufferingVideoSinkTest {
} }
@Test @Test
public void clearPendingOperations_clearsPendingOperations() throws Exception { public void clearPendingOperations_clearsPendingOperations() {
BufferingVideoSink bufferingVideoSink = new BufferingVideoSink(context); BufferingVideoSink bufferingVideoSink = new BufferingVideoSink(context);
VideoSink videoSinkMock = mock(VideoSink.class); VideoSink videoSinkMock = mock(VideoSink.class);