Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 aan 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