mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Various improvements to BufferingVideoSink
PiperOrigin-RevId: 717807436
This commit is contained in:
parent
5421a74d06
commit
635e699965
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user