Skip to content

[video_player_android] Modify to use handlesCropAndRotation to detect the SurfaceTexture Impeller backend #9107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/video_player/video_player_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 2.8.3

* Changes plugin to use `TextureRegistry.SurfaceProducer.handlesCropAndRotation` to detect
whether or not the video player rotation needs to be corrected.

## 2.8.2

* Fixes a [bug](https://github.com/flutter/flutter/issues/164689) that can cause video to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
import static androidx.media3.common.Player.REPEAT_MODE_OFF;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.exoplayer.ExoPlayer;
import io.flutter.view.TextureRegistry.SurfaceProducer;

/**
* A class responsible for managing video playback using {@link ExoPlayer}.
Expand All @@ -24,6 +26,7 @@ public abstract class VideoPlayer {
@NonNull private final MediaItem mediaItem;
@NonNull private final VideoPlayerOptions options;
@NonNull protected final VideoPlayerCallbacks videoPlayerEvents;
@Nullable protected final SurfaceProducer surfaceProducer;
@NonNull protected ExoPlayer exoPlayer;

/** A closure-compatible signature since {@link java.util.function.Supplier} is API level 24. */
Expand All @@ -41,11 +44,13 @@ public VideoPlayer(
@NonNull VideoPlayerCallbacks events,
@NonNull MediaItem mediaItem,
@NonNull VideoPlayerOptions options,
@Nullable SurfaceProducer surfaceProducer,
@NonNull ExoPlayerProvider exoPlayerProvider) {
this.videoPlayerEvents = events;
this.mediaItem = mediaItem;
this.options = options;
this.exoPlayerProvider = exoPlayerProvider;
this.surfaceProducer = surfaceProducer;
this.exoPlayer = createVideoPlayer();
}

Expand All @@ -54,16 +59,15 @@ protected ExoPlayer createVideoPlayer() {
ExoPlayer exoPlayer = exoPlayerProvider.get();
exoPlayer.setMediaItem(mediaItem);
exoPlayer.prepare();

exoPlayer.addListener(createExoPlayerEventListener(exoPlayer));
exoPlayer.addListener(createExoPlayerEventListener(exoPlayer, surfaceProducer));
setAudioAttributes(exoPlayer, options.mixWithOthers);

return exoPlayer;
}

@NonNull
protected abstract ExoPlayerEventListener createExoPlayerEventListener(
@NonNull ExoPlayer exoPlayer);
@NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer);

void sendBufferingUpdate() {
videoPlayerEvents.onBufferingUpdate(exoPlayer.getBufferedPosition());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.MediaItem;
import androidx.media3.exoplayer.ExoPlayer;
Expand All @@ -14,6 +15,7 @@
import io.flutter.plugins.videoplayer.VideoPlayer;
import io.flutter.plugins.videoplayer.VideoPlayerCallbacks;
import io.flutter.plugins.videoplayer.VideoPlayerOptions;
import io.flutter.view.TextureRegistry.SurfaceProducer;

/**
* A subclass of {@link VideoPlayer} that adds functionality related to platform view as a way of
Expand All @@ -26,7 +28,7 @@ public PlatformViewVideoPlayer(
@NonNull MediaItem mediaItem,
@NonNull VideoPlayerOptions options,
@NonNull ExoPlayerProvider exoPlayerProvider) {
super(events, mediaItem, options, exoPlayerProvider);
super(events, mediaItem, options, /* surfaceProducer */ null, exoPlayerProvider);
}

/**
Expand Down Expand Up @@ -58,9 +60,10 @@ public static PlatformViewVideoPlayer create(

@NonNull
@Override
protected ExoPlayerEventListener createExoPlayerEventListener(@NonNull ExoPlayer exoPlayer) {
protected ExoPlayerEventListener createExoPlayerEventListener(
@NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer) {
// Platform view video player does not suspend and re-create the exoPlayer, hence initialized
// is always false.
// is always false. It also does not require a reference to the SurfaceProducer.
return new PlatformViewExoPlayerEventListener(exoPlayer, videoPlayerEvents, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,23 @@
import java.util.Objects;

public final class TextureExoPlayerEventListener extends ExoPlayerEventListener {
private boolean surfaceProducerHandlesCropAndRotation;

@VisibleForTesting
public TextureExoPlayerEventListener(
@NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events) {
this(exoPlayer, events, false);
@NonNull ExoPlayer exoPlayer,
@NonNull VideoPlayerCallbacks events,
boolean surfaceProducerHandlesCropAndRotation) {
this(exoPlayer, events, surfaceProducerHandlesCropAndRotation, false);
}

public TextureExoPlayerEventListener(
@NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events, boolean initialized) {
@NonNull ExoPlayer exoPlayer,
@NonNull VideoPlayerCallbacks events,
boolean surfaceProducerHandlesCropAndRotation,
boolean initialized) {
super(exoPlayer, events, initialized);
this.surfaceProducerHandlesCropAndRotation = surfaceProducerHandlesCropAndRotation;
}

@Override
Expand All @@ -51,10 +59,7 @@ protected void sendInitialized() {
reportedRotationCorrection = RotationDegrees.ROTATE_0;
rotationCorrection = 0;
}
}
// TODO(camsim99): Replace this with a call to `handlesCropAndRotation` when it is
// available in stable. https://github.com/flutter/flutter/issues/157198
else if (Build.VERSION.SDK_INT < 29) {
} else if (surfaceProducerHandlesCropAndRotation) {
// When the SurfaceTexture backend for Impeller is used, the preview should already
// be correctly rotated.
rotationCorrection = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import io.flutter.plugins.videoplayer.VideoPlayer;
import io.flutter.plugins.videoplayer.VideoPlayerCallbacks;
import io.flutter.plugins.videoplayer.VideoPlayerOptions;
import io.flutter.view.TextureRegistry;
import io.flutter.view.TextureRegistry.SurfaceProducer;

/**
* A subclass of {@link VideoPlayer} that adds functionality related to texture view as a way of
Expand All @@ -26,9 +26,7 @@
* <p>It manages the lifecycle of the texture and ensures that the video is properly displayed on
* the texture.
*/
public final class TextureVideoPlayer extends VideoPlayer
implements TextureRegistry.SurfaceProducer.Callback {
@NonNull private final TextureRegistry.SurfaceProducer surfaceProducer;
public final class TextureVideoPlayer extends VideoPlayer implements SurfaceProducer.Callback {
@Nullable private ExoPlayerState savedStateDuring;

/**
Expand All @@ -45,7 +43,7 @@ public final class TextureVideoPlayer extends VideoPlayer
public static TextureVideoPlayer create(
@NonNull Context context,
@NonNull VideoPlayerCallbacks events,
@NonNull TextureRegistry.SurfaceProducer surfaceProducer,
@NonNull SurfaceProducer surfaceProducer,
@NonNull VideoAsset asset,
@NonNull VideoPlayerOptions options) {
return new TextureVideoPlayer(
Expand All @@ -64,23 +62,31 @@ public static TextureVideoPlayer create(
@VisibleForTesting
public TextureVideoPlayer(
@NonNull VideoPlayerCallbacks events,
@NonNull TextureRegistry.SurfaceProducer surfaceProducer,
@NonNull SurfaceProducer surfaceProducer,
@NonNull MediaItem mediaItem,
@NonNull VideoPlayerOptions options,
@NonNull ExoPlayerProvider exoPlayerProvider) {
super(events, mediaItem, options, exoPlayerProvider);
super(events, mediaItem, options, surfaceProducer, exoPlayerProvider);

this.surfaceProducer = surfaceProducer;
surfaceProducer.setCallback(this);

this.exoPlayer.setVideoSurface(surfaceProducer.getSurface());
}

@NonNull
@Override
protected ExoPlayerEventListener createExoPlayerEventListener(@NonNull ExoPlayer exoPlayer) {
protected ExoPlayerEventListener createExoPlayerEventListener(
@NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer) {
if (surfaceProducer == null) {
throw new IllegalArgumentException(
"surfaceProducer cannot be null to create an ExoPlayerEventListener for TextureVideoPlayer.");
}
boolean surfaceProducerHandlesCropAndRotation = surfaceProducer.handlesCropAndRotation();
return new TextureExoPlayerEventListener(
exoPlayer, videoPlayerEvents, playerHasBeenSuspended());
exoPlayer,
videoPlayerEvents,
surfaceProducerHandlesCropAndRotation,
playerHasBeenSuspended());
}

@RestrictTo(RestrictTo.Scope.LIBRARY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import androidx.media3.common.VideoSize;
import androidx.media3.exoplayer.ExoPlayer;
import io.flutter.plugins.videoplayer.texture.TextureExoPlayerEventListener;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand All @@ -33,18 +32,28 @@
public class TextureExoPlayerEventListenerTest {
@Mock private ExoPlayer mockExoPlayer;
@Mock private VideoPlayerCallbacks mockCallbacks;
private TextureExoPlayerEventListener eventListener;

@Rule public MockitoRule initRule = MockitoJUnit.rule();

@Before
public void setUp() {
eventListener = new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks);
@Test
@Config(maxSdk = 21)
public void onPlaybackStateChangedReadySendInitialized_belowAndroid21() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this is actually exactly Android21 right, not below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is right because we are testing the sdk <= 21 case, but if I'm missing something, let me know!

TextureExoPlayerEventListener eventListener =
new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
VideoSize size = new VideoSize(800, 400, 0, 0);
when(mockExoPlayer.getVideoSize()).thenReturn(size);
when(mockExoPlayer.getDuration()).thenReturn(10L);

eventListener.onPlaybackStateChanged(Player.STATE_READY);
verify(mockCallbacks).onInitialized(800, 400, 10L, 0);
}

@Test
@Config(maxSdk = 28)
public void onPlaybackStateChangedReadySendInitialized_belowAndroid29() {
@Config(minSdk = 22)
public void
onPlaybackStateChangedReadySendInitialized_whenSurfaceProducerHandlesCropAndRotation() {
TextureExoPlayerEventListener eventListener =
new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
VideoSize size = new VideoSize(800, 400, 0, 0);
when(mockExoPlayer.getVideoSize()).thenReturn(size);
when(mockExoPlayer.getDuration()).thenReturn(10L);
Expand All @@ -54,9 +63,11 @@ public void onPlaybackStateChangedReadySendInitialized_belowAndroid29() {
}

@Test
@Config(minSdk = 29)
@Config(minSdk = 22)
public void
onPlaybackStateChangedReadySendInitializedWithRotationCorrectionAndWidthAndHeightSwap_aboveAndroid29() {
onPlaybackStateChangedReadySendInitializedWithRotationCorrectionAndWidthAndHeightSwap_whenSurfaceProducerDoesNotHandleCropAndRotation() {
TextureExoPlayerEventListener eventListener =
new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, false);
VideoSize size = new VideoSize(800, 400, 0, 0);
int rotationCorrection = 90;
Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build();
Expand All @@ -73,6 +84,8 @@ public void onPlaybackStateChangedReadySendInitialized_belowAndroid29() {
@Config(maxSdk = 21)
public void
onPlaybackStateChangedReadyInPortraitMode90DegreesSwapWidthAndHeight_belowAndroid21() {
TextureExoPlayerEventListener eventListener =
new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
VideoSize size = new VideoSize(800, 400, 90, 0);
when(mockExoPlayer.getVideoSize()).thenReturn(size);
when(mockExoPlayer.getDuration()).thenReturn(10L);
Expand All @@ -82,9 +95,11 @@ public void onPlaybackStateChangedReadySendInitialized_belowAndroid29() {
}

@Test
@Config(minSdk = 22, maxSdk = 28)
@Config(minSdk = 22)
public void
onPlaybackStateChangedReadyInPortraitMode90DegreesDoesNotSwapWidthAndHeight_aboveAndroid21belowAndroid29() {
onPlaybackStateChangedReadyInPortraitMode90DegreesDoesNotSwapWidthAndHeight_whenSurfaceProducerHandlesCropAndRotation() {
TextureExoPlayerEventListener eventListener =
new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
VideoSize size = new VideoSize(800, 400, 90, 0);

when(mockExoPlayer.getVideoSize()).thenReturn(size);
Expand All @@ -95,9 +110,11 @@ public void onPlaybackStateChangedReadySendInitialized_belowAndroid29() {
}

@Test
@Config(minSdk = 29)
@Config(minSdk = 22)
public void
onPlaybackStateChangedReadyInPortraitMode90DegreesSwapWidthAndHeight_aboveAndroid29() {
onPlaybackStateChangedReadyInPortraitMode90DegreesSwapWidthAndHeight_whenSurfaceProducerDoesNotHandleCropAndRotation() {
TextureExoPlayerEventListener eventListener =
new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, false);
VideoSize size = new VideoSize(800, 400, 0, 0);
int rotationCorrection = 90;
Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build();
Expand All @@ -114,6 +131,8 @@ public void onPlaybackStateChangedReadySendInitialized_belowAndroid29() {
@Config(maxSdk = 21)
public void
onPlaybackStateChangedReadyInPortraitMode270DegreesSwapWidthAndHeight_belowAndroid21() {
TextureExoPlayerEventListener eventListener =
new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
VideoSize size = new VideoSize(800, 400, 270, 0);
when(mockExoPlayer.getVideoSize()).thenReturn(size);
when(mockExoPlayer.getDuration()).thenReturn(10L);
Expand All @@ -123,9 +142,11 @@ public void onPlaybackStateChangedReadySendInitialized_belowAndroid29() {
}

@Test
@Config(minSdk = 22, maxSdk = 28)
@Config(minSdk = 22)
public void
onPlaybackStateChangedReadyInPortraitMode270DegreesDoesNotSwapWidthAndHeight_aboveAndroid21belowAndroid29() {
onPlaybackStateChangedReadyInPortraitMode270DegreesDoesNotSwapWidthAndHeight_whenSurfaceProducerHandlesCropAndRotation() {
TextureExoPlayerEventListener eventListener =
new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
VideoSize size = new VideoSize(800, 400, 270, 0);
when(mockExoPlayer.getVideoSize()).thenReturn(size);
when(mockExoPlayer.getDuration()).thenReturn(10L);
Expand All @@ -135,9 +156,11 @@ public void onPlaybackStateChangedReadySendInitialized_belowAndroid29() {
}

@Test
@Config(minSdk = 29)
@Config(minSdk = 22)
public void
onPlaybackStateChangedReadyInPortraitMode270DegreesSwapWidthAndHeight_aboveAndroid29() {
onPlaybackStateChangedReadyInPortraitMode270DegreesDoesNotSwapWidthAndHeight_whenSurfaceProducerDoesNotHandleCropAndRotation() {
TextureExoPlayerEventListener eventListener =
new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, false);
VideoSize size = new VideoSize(800, 400, 0, 0);
int rotationCorrection = 270;
Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build();
Expand All @@ -153,6 +176,8 @@ public void onPlaybackStateChangedReadySendInitialized_belowAndroid29() {
@Test
@Config(maxSdk = 21)
public void onPlaybackStateChangedReadyFlipped180DegreesInformEventHandler_belowAndroid21() {
TextureExoPlayerEventListener eventListener =
new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
VideoSize size = new VideoSize(800, 400, 180, 0);
when(mockExoPlayer.getVideoSize()).thenReturn(size);
when(mockExoPlayer.getDuration()).thenReturn(10L);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public final class TextureVideoPlayerTest {
public void setUp() {
fakeVideoAsset = new FakeVideoAsset(FAKE_ASSET_URL);
when(mockProducer.getSurface()).thenReturn(mock(Surface.class));
when(mockProducer.handlesCropAndRotation()).thenReturn(true);
}

private VideoPlayer createVideoPlayer() {
Expand Down Expand Up @@ -188,6 +189,7 @@ public void onSurfaceAvailableWithoutDestroyDoesNotRecreate() {
// Initially create the video player, which creates the initial surface.
VideoPlayer videoPlayer = createVideoPlayer();
verify(mockProducer).getSurface();
verify(mockProducer).handlesCropAndRotation();

// Capture the lifecycle events so we can simulate onSurfaceAvailable/Destroyed.
verify(mockProducer).setCallback(callbackCaptor.capture());
Expand Down
Loading