Skip to content

Commit 1995a0e

Browse files
authored
[video_player_android] Platform view support (#8466)
This PR adds support for platform views on Android as a way of displaying a video. When creating a video, it's now possible to choose between texture view approach (rendered using `Texture` widget on the Flutter side) and platform view approach (rendered on the native side, using `SurfaceView`). `VideoPlayer` class now has nothing to do with texture. The texture-related code was moved from it to `TextureVideoPlayer` - a subclass of `VideoPlayer` that adds texture functionality. There's also `PlatformViewVideoPlayer` - a subclass of `VideoPlayer` that adds platform view functionality. In the plugin class (`create` method) we create either the platform view version or the texture version based on the parameter (`viewType`) passed in from Flutter side. The functionality is not yet exposed in the app-facing package (only in the example app) - it will be done in a separate PR. The PR does not introduce breaking changes. Related issues: - [#86613](flutter/flutter#86613) - this issue requests platform view support for iOS. Here we also add it for the Android, so that it works on both mobile platforms.
1 parent f7a4e5d commit 1995a0e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2291
-708
lines changed

packages/video_player/video_player_android/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 2.8.0
22

3+
* Adds support for platform views as an optional way of displaying a video.
34
* Suppresses deprecation and removal warnings for
45
`TextureRegistry.SurfaceProducer.onSurfaceDestroyed`.
56

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java

Lines changed: 13 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,18 @@
44

55
package io.flutter.plugins.videoplayer;
66

7-
import android.os.Build;
87
import androidx.annotation.NonNull;
9-
import androidx.annotation.OptIn;
10-
import androidx.media3.common.Format;
118
import androidx.media3.common.PlaybackException;
129
import androidx.media3.common.Player;
13-
import androidx.media3.common.VideoSize;
1410
import androidx.media3.exoplayer.ExoPlayer;
15-
import java.util.Objects;
1611

17-
final class ExoPlayerEventListener implements Player.Listener {
18-
private final ExoPlayer exoPlayer;
19-
private final VideoPlayerCallbacks events;
12+
public abstract class ExoPlayerEventListener implements Player.Listener {
2013
private boolean isBuffering = false;
2114
private boolean isInitialized;
15+
protected final ExoPlayer exoPlayer;
16+
protected final VideoPlayerCallbacks events;
2217

23-
private enum RotationDegrees {
18+
protected enum RotationDegrees {
2419
ROTATE_0(0),
2520
ROTATE_90(90),
2621
ROTATE_180(180),
@@ -46,11 +41,8 @@ public int getDegrees() {
4641
}
4742
}
4843

49-
ExoPlayerEventListener(ExoPlayer exoPlayer, VideoPlayerCallbacks events) {
50-
this(exoPlayer, events, false);
51-
}
52-
53-
ExoPlayerEventListener(ExoPlayer exoPlayer, VideoPlayerCallbacks events, boolean initialized) {
44+
public ExoPlayerEventListener(
45+
@NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events, boolean initialized) {
5446
this.exoPlayer = exoPlayer;
5547
this.events = events;
5648
this.isInitialized = initialized;
@@ -68,90 +60,7 @@ private void setBuffering(boolean buffering) {
6860
}
6961
}
7062

71-
@SuppressWarnings("SuspiciousNameCombination")
72-
private void sendInitialized() {
73-
if (isInitialized) {
74-
return;
75-
}
76-
isInitialized = true;
77-
VideoSize videoSize = exoPlayer.getVideoSize();
78-
int rotationCorrection = 0;
79-
int width = videoSize.width;
80-
int height = videoSize.height;
81-
if (width != 0 && height != 0) {
82-
RotationDegrees reportedRotationCorrection = RotationDegrees.ROTATE_0;
83-
84-
if (Build.VERSION.SDK_INT <= 21) {
85-
// On API 21 and below, Exoplayer may not internally handle rotation correction
86-
// and reports it through VideoSize.unappliedRotationDegrees. We may apply it to
87-
// fix the case of upside-down playback.
88-
try {
89-
reportedRotationCorrection =
90-
RotationDegrees.fromDegrees(videoSize.unappliedRotationDegrees);
91-
rotationCorrection =
92-
getRotationCorrectionFromUnappliedRotation(reportedRotationCorrection);
93-
} catch (IllegalArgumentException e) {
94-
// Unapplied rotation other than 0, 90, 180, 270 reported by VideoSize. Because this is unexpected,
95-
// we apply no rotation correction.
96-
reportedRotationCorrection = RotationDegrees.ROTATE_0;
97-
rotationCorrection = 0;
98-
}
99-
}
100-
// TODO(camsim99): Replace this with a call to `handlesCropAndRotation` when it is
101-
// available in stable. https://github.com/flutter/flutter/issues/157198
102-
else if (Build.VERSION.SDK_INT < 29) {
103-
// When the SurfaceTexture backend for Impeller is used, the preview should already
104-
// be correctly rotated.
105-
rotationCorrection = 0;
106-
} else {
107-
// The video's Format also provides a rotation correction that may be used to
108-
// correct the rotation, so we try to use that to correct the video rotation
109-
// when the ImageReader backend for Impeller is used.
110-
rotationCorrection = getRotationCorrectionFromFormat(exoPlayer);
111-
112-
try {
113-
reportedRotationCorrection = RotationDegrees.fromDegrees(rotationCorrection);
114-
} catch (IllegalArgumentException e) {
115-
// Rotation correction other than 0, 90, 180, 270 reported by Format. Because this is unexpected,
116-
// we apply no rotation correction.
117-
reportedRotationCorrection = RotationDegrees.ROTATE_0;
118-
rotationCorrection = 0;
119-
}
120-
}
121-
122-
// Switch the width/height if video was taken in portrait mode and a rotation
123-
// correction was detected.
124-
if (reportedRotationCorrection == RotationDegrees.ROTATE_90
125-
|| reportedRotationCorrection == RotationDegrees.ROTATE_270) {
126-
width = videoSize.height;
127-
height = videoSize.width;
128-
}
129-
}
130-
events.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection);
131-
}
132-
133-
private int getRotationCorrectionFromUnappliedRotation(RotationDegrees unappliedRotationDegrees) {
134-
int rotationCorrection = 0;
135-
136-
// Rotating the video with ExoPlayer does not seem to be possible with a Surface,
137-
// so inform the Flutter code that the widget needs to be rotated to prevent
138-
// upside-down playback for videos with unappliedRotationDegrees of 180 (other orientations
139-
// work correctly without correction).
140-
if (unappliedRotationDegrees == RotationDegrees.ROTATE_180) {
141-
rotationCorrection = unappliedRotationDegrees.getDegrees();
142-
}
143-
144-
return rotationCorrection;
145-
}
146-
147-
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
148-
// A video's Format and its rotation degrees are unstable because they are not guaranteed
149-
// the same implementation across API versions. It is possible that this logic may need
150-
// revisiting should the implementation change across versions of the Exoplayer API.
151-
private int getRotationCorrectionFromFormat(ExoPlayer exoPlayer) {
152-
Format videoFormat = Objects.requireNonNull(exoPlayer.getVideoFormat());
153-
return videoFormat.rotationDegrees;
154-
}
63+
protected abstract void sendInitialized();
15564

15665
@Override
15766
public void onPlaybackStateChanged(final int playbackState) {
@@ -161,6 +70,10 @@ public void onPlaybackStateChanged(final int playbackState) {
16170
events.onBufferingUpdate(exoPlayer.getBufferedPosition());
16271
break;
16372
case Player.STATE_READY:
73+
if (isInitialized) {
74+
return;
75+
}
76+
isInitialized = true;
16477
sendInitialized();
16578
break;
16679
case Player.STATE_ENDED:
@@ -178,7 +91,8 @@ public void onPlaybackStateChanged(final int playbackState) {
17891
public void onPlayerError(@NonNull final PlaybackException error) {
17992
setBuffering(false);
18093
if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {
181-
// See https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window
94+
// See
95+
// https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window
18296
exoPlayer.seekToDefaultPosition();
18397
exoPlayer.prepare();
18498
} else {

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerState.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package io.flutter.plugins.videoplayer;
66

7+
import androidx.annotation.NonNull;
78
import androidx.media3.common.PlaybackParameters;
89
import androidx.media3.exoplayer.ExoPlayer;
910

@@ -15,7 +16,7 @@
1516
* is reclaimed. Upon <em>resume</em>, the player will need to be recreated, but start again at the
1617
* previous point (and settings).
1718
*/
18-
final class ExoPlayerState {
19+
public final class ExoPlayerState {
1920
/**
2021
* Saves a representation of the current state of the player at the current point in time.
2122
*
@@ -24,12 +25,13 @@ final class ExoPlayerState {
2425
* @param exoPlayer the active player instance.
2526
* @return an opaque object representing the state.
2627
*/
27-
static ExoPlayerState save(ExoPlayer exoPlayer) {
28+
@NonNull
29+
public static ExoPlayerState save(@NonNull ExoPlayer exoPlayer) {
2830
return new ExoPlayerState(
29-
/*position=*/ exoPlayer.getCurrentPosition(),
30-
/*repeatMode=*/ exoPlayer.getRepeatMode(),
31-
/*volume=*/ exoPlayer.getVolume(),
32-
/*playbackParameters=*/ exoPlayer.getPlaybackParameters());
31+
/* position= */ exoPlayer.getCurrentPosition(),
32+
/* repeatMode= */ exoPlayer.getRepeatMode(),
33+
/* volume= */ exoPlayer.getVolume(),
34+
/* playbackParameters= */ exoPlayer.getPlaybackParameters());
3335
}
3436

3537
private ExoPlayerState(
@@ -60,7 +62,7 @@ private ExoPlayerState(
6062
*
6163
* @param exoPlayer the new player instance to reflect the state back to.
6264
*/
63-
void restore(ExoPlayer exoPlayer) {
65+
public void restore(@NonNull ExoPlayer exoPlayer) {
6466
exoPlayer.seekTo(position);
6567
exoPlayer.setRepeatMode(repeatMode);
6668
exoPlayer.setVolume(volume);

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/HttpVideoAsset.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ final class HttpVideoAsset extends VideoAsset {
3737

3838
@NonNull
3939
@Override
40-
MediaItem getMediaItem() {
40+
public MediaItem getMediaItem() {
4141
MediaItem.Builder builder = new MediaItem.Builder().setUri(assetUrl);
4242
String mimeType = null;
4343
switch (streamingFormat) {
@@ -57,8 +57,9 @@ MediaItem getMediaItem() {
5757
return builder.build();
5858
}
5959

60+
@NonNull
6061
@Override
61-
MediaSource.Factory getMediaSourceFactory(Context context) {
62+
public MediaSource.Factory getMediaSourceFactory(@NonNull Context context) {
6263
return getMediaSourceFactory(context, new DefaultHttpDataSource.Factory());
6364
}
6465

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/LocalVideoAsset.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ final class LocalVideoAsset extends VideoAsset {
1717

1818
@NonNull
1919
@Override
20-
MediaItem getMediaItem() {
20+
public MediaItem getMediaItem() {
2121
return new MediaItem.Builder().setUri(assetUrl).build();
2222
}
2323

24+
@NonNull
2425
@Override
25-
MediaSource.Factory getMediaSourceFactory(Context context) {
26+
public MediaSource.Factory getMediaSourceFactory(@NonNull Context context) {
2627
return new DefaultMediaSourceFactory(context);
2728
}
2829
}

0 commit comments

Comments
 (0)