Skip to content

Commit 62b4cb0

Browse files
authored
[video_player] Relands #6456: Uses SurfaceProducer, this time with setCallback for suspend/resume lifecycles. (#6989)
_� BLOCKED: I guess this can't land until the new API makes it into stable in a week or two?_ --- Effectively enough towards flutter/flutter#148417, but we still need to document it on flutter.dev. This is the last _technical_ PR I'll work on towards the plugin work (assuming we don't find additional bugs/issues). /cc @jonahwilliams @chinmaygarde @johnmccutchan.
1 parent 1cf868f commit 62b4cb0

File tree

6 files changed

+186
-55
lines changed

6 files changed

+186
-55
lines changed

packages/video_player/video_player_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.7.0
2+
3+
* Re-adds [support for Impeller](https://docs.flutter.dev/release/breaking-changes/android-surface-plugins).
4+
15
## 2.6.0
26

37
* Adds RTSP support.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.videoplayer;
6+
7+
import androidx.media3.common.PlaybackParameters;
8+
import androidx.media3.exoplayer.ExoPlayer;
9+
10+
/**
11+
* Internal state representing an {@link ExoPlayer} instance at a snapshot in time.
12+
*
13+
* <p>During the Android application lifecycle, the underlying {@link android.view.Surface} being
14+
* rendered to by the player can be destroyed when the application is in the background and memory
15+
* is reclaimed. Upon <em>resume</em>, the player will need to be recreated, but start again at the
16+
* previous point (and settings).
17+
*/
18+
final class ExoPlayerState {
19+
/**
20+
* Saves a representation of the current state of the player at the current point in time.
21+
*
22+
* <p>The inverse of this operation is {@link #restore(ExoPlayer)}.
23+
*
24+
* @param exoPlayer the active player instance.
25+
* @return an opaque object representing the state.
26+
*/
27+
static ExoPlayerState save(ExoPlayer exoPlayer) {
28+
return new ExoPlayerState(
29+
/*position=*/ exoPlayer.getCurrentPosition(),
30+
/*repeatMode=*/ exoPlayer.getRepeatMode(),
31+
/*volume=*/ exoPlayer.getVolume(),
32+
/*playbackParameters=*/ exoPlayer.getPlaybackParameters());
33+
}
34+
35+
private ExoPlayerState(
36+
long position, int repeatMode, float volume, PlaybackParameters playbackParameters) {
37+
this.position = position;
38+
this.repeatMode = repeatMode;
39+
this.volume = volume;
40+
this.playbackParameters = playbackParameters;
41+
}
42+
43+
/** Previous value of {@link ExoPlayer#getCurrentPosition()}. */
44+
private final long position;
45+
46+
/** Previous value of {@link ExoPlayer#getRepeatMode()}. */
47+
private final int repeatMode;
48+
49+
/** Previous value of {@link ExoPlayer#getVolume()}. */
50+
private final float volume;
51+
52+
/** Previous value of {@link ExoPlayer#getPlaybackParameters()}. */
53+
private final PlaybackParameters playbackParameters;
54+
55+
/**
56+
* Restores the captured state onto the provided player.
57+
*
58+
* <p>This will typically be done after creating a new player, setting up a media source, and
59+
* listening to events.
60+
*
61+
* @param exoPlayer the new player instance to reflect the state back to.
62+
*/
63+
void restore(ExoPlayer exoPlayer) {
64+
exoPlayer.seekTo(position);
65+
exoPlayer.setRepeatMode(repeatMode);
66+
exoPlayer.setVolume(volume);
67+
exoPlayer.setPlaybackParameters(playbackParameters);
68+
}
69+
}

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

Lines changed: 73 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
import static androidx.media3.common.Player.REPEAT_MODE_OFF;
99

1010
import android.content.Context;
11-
import android.view.Surface;
1211
import androidx.annotation.NonNull;
12+
import androidx.annotation.Nullable;
13+
import androidx.annotation.RestrictTo;
1314
import androidx.annotation.VisibleForTesting;
1415
import androidx.media3.common.AudioAttributes;
1516
import androidx.media3.common.C;
@@ -18,60 +19,97 @@
1819
import androidx.media3.exoplayer.ExoPlayer;
1920
import io.flutter.view.TextureRegistry;
2021

21-
final class VideoPlayer {
22-
private ExoPlayer exoPlayer;
23-
private Surface surface;
24-
private final TextureRegistry.SurfaceTextureEntry textureEntry;
25-
private final VideoPlayerCallbacks videoPlayerEvents;
26-
private final VideoPlayerOptions options;
22+
final class VideoPlayer implements TextureRegistry.SurfaceProducer.Callback {
23+
@NonNull private final ExoPlayerProvider exoPlayerProvider;
24+
@NonNull private final MediaItem mediaItem;
25+
@NonNull private final TextureRegistry.SurfaceProducer surfaceProducer;
26+
@NonNull private final VideoPlayerCallbacks videoPlayerEvents;
27+
@NonNull private final VideoPlayerOptions options;
28+
@NonNull private ExoPlayer exoPlayer;
29+
@Nullable private ExoPlayerState savedStateDuring;
2730

2831
/**
2932
* Creates a video player.
3033
*
3134
* @param context application context.
3235
* @param events event callbacks.
33-
* @param textureEntry texture to render to.
36+
* @param surfaceProducer produces a texture to render to.
3437
* @param asset asset to play.
3538
* @param options options for playback.
3639
* @return a video player instance.
3740
*/
3841
@NonNull
3942
static VideoPlayer create(
40-
Context context,
41-
VideoPlayerCallbacks events,
42-
TextureRegistry.SurfaceTextureEntry textureEntry,
43-
VideoAsset asset,
44-
VideoPlayerOptions options) {
45-
ExoPlayer.Builder builder =
46-
new ExoPlayer.Builder(context).setMediaSourceFactory(asset.getMediaSourceFactory(context));
47-
return new VideoPlayer(builder, events, textureEntry, asset.getMediaItem(), options);
43+
@NonNull Context context,
44+
@NonNull VideoPlayerCallbacks events,
45+
@NonNull TextureRegistry.SurfaceProducer surfaceProducer,
46+
@NonNull VideoAsset asset,
47+
@NonNull VideoPlayerOptions options) {
48+
return new VideoPlayer(
49+
() -> {
50+
ExoPlayer.Builder builder =
51+
new ExoPlayer.Builder(context)
52+
.setMediaSourceFactory(asset.getMediaSourceFactory(context));
53+
return builder.build();
54+
},
55+
events,
56+
surfaceProducer,
57+
asset.getMediaItem(),
58+
options);
59+
}
60+
61+
/** A closure-compatible signature since {@link java.util.function.Supplier} is API level 24. */
62+
interface ExoPlayerProvider {
63+
/**
64+
* Returns a new {@link ExoPlayer}.
65+
*
66+
* @return new instance.
67+
*/
68+
ExoPlayer get();
4869
}
4970

5071
@VisibleForTesting
5172
VideoPlayer(
52-
ExoPlayer.Builder builder,
53-
VideoPlayerCallbacks events,
54-
TextureRegistry.SurfaceTextureEntry textureEntry,
55-
MediaItem mediaItem,
56-
VideoPlayerOptions options) {
73+
@NonNull ExoPlayerProvider exoPlayerProvider,
74+
@NonNull VideoPlayerCallbacks events,
75+
@NonNull TextureRegistry.SurfaceProducer surfaceProducer,
76+
@NonNull MediaItem mediaItem,
77+
@NonNull VideoPlayerOptions options) {
78+
this.exoPlayerProvider = exoPlayerProvider;
5779
this.videoPlayerEvents = events;
58-
this.textureEntry = textureEntry;
80+
this.surfaceProducer = surfaceProducer;
81+
this.mediaItem = mediaItem;
5982
this.options = options;
83+
this.exoPlayer = createVideoPlayer();
84+
surfaceProducer.setCallback(this);
85+
}
6086

61-
ExoPlayer exoPlayer = builder.build();
62-
exoPlayer.setMediaItem(mediaItem);
63-
exoPlayer.prepare();
87+
@RestrictTo(RestrictTo.Scope.LIBRARY)
88+
public void onSurfaceCreated() {
89+
exoPlayer = createVideoPlayer();
90+
if (savedStateDuring != null) {
91+
savedStateDuring.restore(exoPlayer);
92+
savedStateDuring = null;
93+
}
94+
}
6495

65-
setUpVideoPlayer(exoPlayer);
96+
@RestrictTo(RestrictTo.Scope.LIBRARY)
97+
public void onSurfaceDestroyed() {
98+
exoPlayer.stop();
99+
savedStateDuring = ExoPlayerState.save(exoPlayer);
100+
exoPlayer.release();
66101
}
67102

68-
private void setUpVideoPlayer(ExoPlayer exoPlayer) {
69-
this.exoPlayer = exoPlayer;
103+
private ExoPlayer createVideoPlayer() {
104+
ExoPlayer exoPlayer = exoPlayerProvider.get();
105+
exoPlayer.setMediaItem(mediaItem);
106+
exoPlayer.prepare();
70107

71-
surface = new Surface(textureEntry.surfaceTexture());
72-
exoPlayer.setVideoSurface(surface);
73-
setAudioAttributes(exoPlayer, options.mixWithOthers);
108+
exoPlayer.setVideoSurface(surfaceProducer.getSurface());
74109
exoPlayer.addListener(new ExoPlayerEventListener(exoPlayer, videoPlayerEvents));
110+
setAudioAttributes(exoPlayer, options.mixWithOthers);
111+
112+
return exoPlayer;
75113
}
76114

77115
void sendBufferingUpdate() {
@@ -85,11 +123,11 @@ private static void setAudioAttributes(ExoPlayer exoPlayer, boolean isMixMode) {
85123
}
86124

87125
void play() {
88-
exoPlayer.setPlayWhenReady(true);
126+
exoPlayer.play();
89127
}
90128

91129
void pause() {
92-
exoPlayer.setPlayWhenReady(false);
130+
exoPlayer.pause();
93131
}
94132

95133
void setLooping(boolean value) {
@@ -118,12 +156,7 @@ long getPosition() {
118156
}
119157

120158
void dispose() {
121-
textureEntry.release();
122-
if (surface != null) {
123-
surface.release();
124-
}
125-
if (exoPlayer != null) {
126-
exoPlayer.release();
127-
}
159+
surfaceProducer.release();
160+
exoPlayer.release();
128161
}
129162
}

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import io.flutter.view.TextureRegistry;
2525
import java.security.KeyManagementException;
2626
import java.security.NoSuchAlgorithmException;
27-
import java.util.Map;
2827
import javax.net.ssl.HttpsURLConnection;
2928

3029
/** Android platform implementation of the VideoPlayerPlugin. */
@@ -94,8 +93,7 @@ public void initialize() {
9493
}
9594

9695
public @NonNull TextureMessage create(@NonNull CreateMessage arg) {
97-
TextureRegistry.SurfaceTextureEntry handle =
98-
flutterState.textureRegistry.createSurfaceTexture();
96+
TextureRegistry.SurfaceProducer handle = flutterState.textureRegistry.createSurfaceProducer();
9997
EventChannel eventChannel =
10098
new EventChannel(
10199
flutterState.binaryMessenger, "flutter.io/videoPlayer/videoEvents" + handle.id());
@@ -113,7 +111,6 @@ public void initialize() {
113111
} else if (arg.getUri().startsWith("rtsp://")) {
114112
videoAsset = VideoAsset.fromRtspUrl(arg.getUri());
115113
} else {
116-
Map<String, String> httpHeaders = arg.getHttpHeaders();
117114
VideoAsset.StreamingFormat streamingFormat = VideoAsset.StreamingFormat.UNKNOWN;
118115
String formatHint = arg.getFormatHint();
119116
if (formatHint != null) {

packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import static org.mockito.ArgumentMatchers.any;
99
import static org.mockito.Mockito.*;
1010

11-
import android.graphics.SurfaceTexture;
11+
import android.view.Surface;
1212
import androidx.media3.common.AudioAttributes;
1313
import androidx.media3.common.C;
1414
import androidx.media3.common.PlaybackParameters;
@@ -44,18 +44,17 @@ public final class VideoPlayerTest {
4444
private FakeVideoAsset fakeVideoAsset;
4545

4646
@Mock private VideoPlayerCallbacks mockEvents;
47-
@Mock private TextureRegistry.SurfaceTextureEntry mockTexture;
48-
@Mock private ExoPlayer.Builder mockBuilder;
47+
@Mock private TextureRegistry.SurfaceProducer mockProducer;
4948
@Mock private ExoPlayer mockExoPlayer;
5049
@Captor private ArgumentCaptor<AudioAttributes> attributesCaptor;
50+
@Captor private ArgumentCaptor<TextureRegistry.SurfaceProducer.Callback> callbackCaptor;
5151

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

5454
@Before
5555
public void setUp() {
5656
fakeVideoAsset = new FakeVideoAsset(FAKE_ASSET_URL);
57-
when(mockBuilder.build()).thenReturn(mockExoPlayer);
58-
when(mockTexture.surfaceTexture()).thenReturn(mock(SurfaceTexture.class));
57+
when(mockProducer.getSurface()).thenReturn(mock(Surface.class));
5958
}
6059

6160
private VideoPlayer createVideoPlayer() {
@@ -64,7 +63,7 @@ private VideoPlayer createVideoPlayer() {
6463

6564
private VideoPlayer createVideoPlayer(VideoPlayerOptions options) {
6665
return new VideoPlayer(
67-
mockBuilder, mockEvents, mockTexture, fakeVideoAsset.getMediaItem(), options);
66+
() -> mockExoPlayer, mockEvents, mockProducer, fakeVideoAsset.getMediaItem(), options);
6867
}
6968

7069
@Test
@@ -73,7 +72,7 @@ public void loadsAndPreparesProvidedMediaEnablesAudioFocusByDefault() {
7372

7473
verify(mockExoPlayer).setMediaItem(fakeVideoAsset.getMediaItem());
7574
verify(mockExoPlayer).prepare();
76-
verify(mockTexture).surfaceTexture();
75+
verify(mockProducer).getSurface();
7776
verify(mockExoPlayer).setVideoSurface(any());
7877

7978
verify(mockExoPlayer).setAudioAttributes(attributesCaptor.capture(), eq(true));
@@ -100,10 +99,10 @@ public void playsAndPausesProvidedMedia() {
10099
VideoPlayer videoPlayer = createVideoPlayer();
101100

102101
videoPlayer.play();
103-
verify(mockExoPlayer).setPlayWhenReady(true);
102+
verify(mockExoPlayer).play();
104103

105104
videoPlayer.pause();
106-
verify(mockExoPlayer).setPlayWhenReady(false);
105+
verify(mockExoPlayer).pause();
107106

108107
videoPlayer.dispose();
109108
}
@@ -169,12 +168,41 @@ public void seekAndGetPosition() {
169168
assertEquals(20L, videoPlayer.getPosition());
170169
}
171170

171+
@Test
172+
public void onSurfaceProducerDestroyedAndRecreatedReleasesAndThenRecreatesAndResumesPlayer() {
173+
VideoPlayer videoPlayer = createVideoPlayer();
174+
175+
verify(mockProducer).setCallback(callbackCaptor.capture());
176+
verify(mockExoPlayer, never()).release();
177+
178+
when(mockExoPlayer.getCurrentPosition()).thenReturn(10L);
179+
when(mockExoPlayer.getRepeatMode()).thenReturn(Player.REPEAT_MODE_ALL);
180+
when(mockExoPlayer.getVolume()).thenReturn(0.5f);
181+
when(mockExoPlayer.getPlaybackParameters()).thenReturn(new PlaybackParameters(2.5f));
182+
183+
TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue();
184+
producerLifecycle.onSurfaceDestroyed();
185+
186+
verify(mockExoPlayer).release();
187+
188+
// Create a new mock exo player so that we get a new instance.
189+
mockExoPlayer = mock(ExoPlayer.class);
190+
producerLifecycle.onSurfaceCreated();
191+
192+
verify(mockExoPlayer).seekTo(10L);
193+
verify(mockExoPlayer).setRepeatMode(Player.REPEAT_MODE_ALL);
194+
verify(mockExoPlayer).setVolume(0.5f);
195+
verify(mockExoPlayer).setPlaybackParameters(new PlaybackParameters(2.5f));
196+
197+
videoPlayer.dispose();
198+
}
199+
172200
@Test
173201
public void disposeReleasesTextureAndPlayer() {
174202
VideoPlayer videoPlayer = createVideoPlayer();
175203
videoPlayer.dispose();
176204

177-
verify(mockTexture).release();
205+
verify(mockProducer).release();
178206
verify(mockExoPlayer).release();
179207
}
180208
}

packages/video_player/video_player_android/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: video_player_android
22
description: Android implementation of the video_player plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
5-
version: 2.6.0
5+
version: 2.7.0
66

77
environment:
88
sdk: ^3.4.0

0 commit comments

Comments
 (0)