Skip to content

Commit 1ad8d89

Browse files
authored
Refactor VideoPlayer to be less exposed to EventChannel & related (flutter#6908)
Part of flutter#148417. I'm working on re-landing flutter/packages#6456, this time without using the `ActivityAware` interface (see flutter#148417). As part of that work, I'll need to better control the `ExoPlayer` lifecycle and save/restore internal state. These are some proposed refactors to limit how much work `VideoPlayer` is doing, so I can better understand what needs to be reset (or not) internally. Specifically, `VideoPlayer` no longer knows what an `EventChannel` or `EventSink` is, and does not need to manage the lifecycle (it stores a `private final VideoPlayerCallbacks` instead), and instead there is a `VideoPlayerCallbacks` interface that does all that. I'm totally open to: - Landing this as-is (+/- nits) and making minor improvements in follow-up PRs - Making more significant changes to this PR and then landing it - Not landing this PR at all because it doesn't follow the approach the folks who maintain the plugin prefer Also happy to chat in VC/person about any of the changes.
1 parent 0891835 commit 1ad8d89

File tree

5 files changed

+256
-132
lines changed

5 files changed

+256
-132
lines changed

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

Lines changed: 45 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,7 @@
2828
import androidx.media3.datasource.DefaultHttpDataSource;
2929
import androidx.media3.exoplayer.ExoPlayer;
3030
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
31-
import io.flutter.plugin.common.EventChannel;
3231
import io.flutter.view.TextureRegistry;
33-
import java.util.Arrays;
34-
import java.util.Collections;
35-
import java.util.HashMap;
36-
import java.util.List;
3732
import java.util.Map;
3833

3934
final class VideoPlayer {
@@ -48,9 +43,7 @@ final class VideoPlayer {
4843

4944
private final TextureRegistry.SurfaceTextureEntry textureEntry;
5045

51-
private QueuingEventSink eventSink;
52-
53-
private final EventChannel eventChannel;
46+
private final VideoPlayerCallbacks videoPlayerEvents;
5447

5548
private static final String USER_AGENT = "User-Agent";
5649

@@ -62,13 +55,13 @@ final class VideoPlayer {
6255

6356
VideoPlayer(
6457
Context context,
65-
EventChannel eventChannel,
58+
VideoPlayerCallbacks events,
6659
TextureRegistry.SurfaceTextureEntry textureEntry,
6760
String dataSource,
6861
String formatHint,
6962
@NonNull Map<String, String> httpHeaders,
7063
VideoPlayerOptions options) {
71-
this.eventChannel = eventChannel;
64+
this.videoPlayerEvents = events;
7265
this.textureEntry = textureEntry;
7366
this.options = options;
7467

@@ -86,24 +79,23 @@ final class VideoPlayer {
8679
exoPlayer.setMediaItem(mediaItem);
8780
exoPlayer.prepare();
8881

89-
setUpVideoPlayer(exoPlayer, new QueuingEventSink());
82+
setUpVideoPlayer(exoPlayer);
9083
}
9184

9285
// Constructor used to directly test members of this class.
9386
@VisibleForTesting
9487
VideoPlayer(
9588
ExoPlayer exoPlayer,
96-
EventChannel eventChannel,
89+
VideoPlayerCallbacks events,
9790
TextureRegistry.SurfaceTextureEntry textureEntry,
9891
VideoPlayerOptions options,
99-
QueuingEventSink eventSink,
10092
DefaultHttpDataSource.Factory httpDataSourceFactory) {
101-
this.eventChannel = eventChannel;
93+
this.videoPlayerEvents = events;
10294
this.textureEntry = textureEntry;
10395
this.options = options;
10496
this.httpDataSourceFactory = httpDataSourceFactory;
10597

106-
setUpVideoPlayer(exoPlayer, eventSink);
98+
setUpVideoPlayer(exoPlayer);
10799
}
108100

109101
@VisibleForTesting
@@ -118,37 +110,29 @@ public void configureHttpDataSourceFactory(@NonNull Map<String, String> httpHead
118110
httpDataSourceFactory, httpHeaders, userAgent, httpHeadersNotEmpty);
119111
}
120112

121-
private void setUpVideoPlayer(ExoPlayer exoPlayer, QueuingEventSink eventSink) {
113+
private void setUpVideoPlayer(ExoPlayer exoPlayer) {
122114
this.exoPlayer = exoPlayer;
123-
this.eventSink = eventSink;
124-
125-
eventChannel.setStreamHandler(
126-
new EventChannel.StreamHandler() {
127-
@Override
128-
public void onListen(Object o, EventChannel.EventSink sink) {
129-
eventSink.setDelegate(sink);
130-
}
131-
132-
@Override
133-
public void onCancel(Object o) {
134-
eventSink.setDelegate(null);
135-
}
136-
});
137115

138116
surface = new Surface(textureEntry.surfaceTexture());
139117
exoPlayer.setVideoSurface(surface);
140118
setAudioAttributes(exoPlayer, options.mixWithOthers);
141119

120+
// Avoids synthetic accessor.
121+
VideoPlayerCallbacks events = this.videoPlayerEvents;
122+
142123
exoPlayer.addListener(
143124
new Listener() {
144125
private boolean isBuffering = false;
145126

146127
public void setBuffering(boolean buffering) {
147-
if (isBuffering != buffering) {
148-
isBuffering = buffering;
149-
Map<String, Object> event = new HashMap<>();
150-
event.put("event", isBuffering ? "bufferingStart" : "bufferingEnd");
151-
eventSink.success(event);
128+
if (isBuffering == buffering) {
129+
return;
130+
}
131+
isBuffering = buffering;
132+
if (buffering) {
133+
events.onBufferingStart();
134+
} else {
135+
events.onBufferingEnd();
152136
}
153137
}
154138

@@ -163,11 +147,8 @@ public void onPlaybackStateChanged(final int playbackState) {
163147
sendInitialized();
164148
}
165149
} else if (playbackState == Player.STATE_ENDED) {
166-
Map<String, Object> event = new HashMap<>();
167-
event.put("event", "completed");
168-
eventSink.success(event);
150+
events.onCompleted();
169151
}
170-
171152
if (playbackState != Player.STATE_BUFFERING) {
172153
setBuffering(false);
173154
}
@@ -180,30 +161,20 @@ public void onPlayerError(@NonNull final PlaybackException error) {
180161
// See https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window
181162
exoPlayer.seekToDefaultPosition();
182163
exoPlayer.prepare();
183-
} else if (eventSink != null) {
184-
eventSink.error("VideoError", "Video player had error " + error, null);
164+
} else {
165+
events.onError("VideoError", "Video player had error " + error, null);
185166
}
186167
}
187168

188169
@Override
189170
public void onIsPlayingChanged(boolean isPlaying) {
190-
if (eventSink != null) {
191-
Map<String, Object> event = new HashMap<>();
192-
event.put("event", "isPlayingStateUpdate");
193-
event.put("isPlaying", isPlaying);
194-
eventSink.success(event);
195-
}
171+
events.onIsPlayingStateUpdate(isPlaying);
196172
}
197173
});
198174
}
199175

200176
void sendBufferingUpdate() {
201-
Map<String, Object> event = new HashMap<>();
202-
event.put("event", "bufferingUpdate");
203-
List<? extends Number> range = Arrays.asList(0, exoPlayer.getBufferedPosition());
204-
// iOS supports a list of buffered ranges, so here is a list with a single range.
205-
event.put("values", Collections.singletonList(range));
206-
eventSink.success(event);
177+
videoPlayerEvents.onBufferingUpdate(exoPlayer.getBufferedPosition());
207178
}
208179

209180
private static void setAudioAttributes(ExoPlayer exoPlayer, boolean isMixMode) {
@@ -248,43 +219,36 @@ long getPosition() {
248219
@SuppressWarnings("SuspiciousNameCombination")
249220
@VisibleForTesting
250221
void sendInitialized() {
251-
if (isInitialized) {
252-
Map<String, Object> event = new HashMap<>();
253-
event.put("event", "initialized");
254-
event.put("duration", exoPlayer.getDuration());
255-
256-
VideoSize videoSize = exoPlayer.getVideoSize();
257-
int width = videoSize.width;
258-
int height = videoSize.height;
259-
if (width != 0 && height != 0) {
260-
int rotationDegrees = videoSize.unappliedRotationDegrees;
261-
// Switch the width/height if video was taken in portrait mode
262-
if (rotationDegrees == 90 || rotationDegrees == 270) {
263-
width = videoSize.height;
264-
height = videoSize.width;
265-
}
266-
event.put("width", width);
267-
event.put("height", height);
268-
269-
// Rotating the video with ExoPlayer does not seem to be possible with a Surface,
270-
// so inform the Flutter code that the widget needs to be rotated to prevent
271-
// upside-down playback for videos with rotationDegrees of 180 (other orientations work
272-
// correctly without correction).
273-
if (rotationDegrees == 180) {
274-
event.put("rotationCorrection", rotationDegrees);
275-
}
222+
if (!isInitialized) {
223+
return;
224+
}
225+
VideoSize videoSize = exoPlayer.getVideoSize();
226+
int rotationCorrection = 0;
227+
int width = videoSize.width;
228+
int height = videoSize.height;
229+
if (width != 0 && height != 0) {
230+
int rotationDegrees = videoSize.unappliedRotationDegrees;
231+
// Switch the width/height if video was taken in portrait mode
232+
if (rotationDegrees == 90 || rotationDegrees == 270) {
233+
width = videoSize.height;
234+
height = videoSize.width;
235+
}
236+
// Rotating the video with ExoPlayer does not seem to be possible with a Surface,
237+
// so inform the Flutter code that the widget needs to be rotated to prevent
238+
// upside-down playback for videos with rotationDegrees of 180 (other orientations work
239+
// correctly without correction).
240+
if (rotationDegrees == 180) {
241+
rotationCorrection = rotationDegrees;
276242
}
277-
278-
eventSink.success(event);
279243
}
244+
videoPlayerEvents.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection);
280245
}
281246

282247
void dispose() {
283248
if (isInitialized) {
284249
exoPlayer.stop();
285250
}
286251
textureEntry.release();
287-
eventChannel.setStreamHandler(null);
288252
if (surface != null) {
289253
surface.release();
290254
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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.annotation.NonNull;
8+
import androidx.annotation.Nullable;
9+
10+
/**
11+
* Callbacks representing events invoked by {@link VideoPlayer}.
12+
*
13+
* <p>In the actual plugin, this will always be {@link VideoPlayerEventCallbacks}, which creates the
14+
* expected events to send back through the plugin channel. In tests methods can be overridden in
15+
* order to assert results.
16+
*
17+
* <p>See {@link androidx.media3.common.Player.Listener} for details.
18+
*/
19+
interface VideoPlayerCallbacks {
20+
void onInitialized(int width, int height, long durationInMs, int rotationCorrectionInDegrees);
21+
22+
void onBufferingStart();
23+
24+
void onBufferingUpdate(long bufferedPosition);
25+
26+
void onBufferingEnd();
27+
28+
void onCompleted();
29+
30+
void onError(@NonNull String code, @Nullable String message, @Nullable Object details);
31+
32+
void onIsPlayingStateUpdate(boolean isPlaying);
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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.annotation.NonNull;
8+
import androidx.annotation.Nullable;
9+
import androidx.annotation.VisibleForTesting;
10+
import io.flutter.plugin.common.EventChannel;
11+
import java.util.Collections;
12+
import java.util.HashMap;
13+
import java.util.Map;
14+
15+
final class VideoPlayerEventCallbacks implements VideoPlayerCallbacks {
16+
private final EventChannel.EventSink eventSink;
17+
18+
static VideoPlayerEventCallbacks bindTo(EventChannel eventChannel) {
19+
QueuingEventSink eventSink = new QueuingEventSink();
20+
eventChannel.setStreamHandler(
21+
new EventChannel.StreamHandler() {
22+
@Override
23+
public void onListen(Object arguments, EventChannel.EventSink events) {
24+
eventSink.setDelegate(events);
25+
}
26+
27+
@Override
28+
public void onCancel(Object arguments) {
29+
eventSink.setDelegate(null);
30+
}
31+
});
32+
return VideoPlayerEventCallbacks.withSink(eventSink);
33+
}
34+
35+
@VisibleForTesting
36+
static VideoPlayerEventCallbacks withSink(EventChannel.EventSink eventSink) {
37+
return new VideoPlayerEventCallbacks(eventSink);
38+
}
39+
40+
private VideoPlayerEventCallbacks(EventChannel.EventSink eventSink) {
41+
this.eventSink = eventSink;
42+
}
43+
44+
@Override
45+
public void onInitialized(
46+
int width, int height, long durationInMs, int rotationCorrectionInDegrees) {
47+
Map<String, Object> event = new HashMap<>();
48+
event.put("event", "initialized");
49+
event.put("width", width);
50+
event.put("height", height);
51+
event.put("duration", durationInMs);
52+
if (rotationCorrectionInDegrees != 0) {
53+
event.put("rotationCorrection", rotationCorrectionInDegrees);
54+
}
55+
eventSink.success(event);
56+
}
57+
58+
@Override
59+
public void onBufferingStart() {
60+
Map<String, Object> event = new HashMap<>();
61+
event.put("event", "bufferingStart");
62+
eventSink.success(event);
63+
}
64+
65+
@Override
66+
public void onBufferingUpdate(long bufferedPosition) {
67+
// iOS supports a list of buffered ranges, so we send as a list with a single range.
68+
Map<String, Object> event = new HashMap<>();
69+
event.put("values", Collections.singletonList(bufferedPosition));
70+
eventSink.success(event);
71+
}
72+
73+
@Override
74+
public void onBufferingEnd() {
75+
Map<String, Object> event = new HashMap<>();
76+
event.put("event", "bufferingEnd");
77+
eventSink.success(event);
78+
}
79+
80+
@Override
81+
public void onCompleted() {
82+
Map<String, Object> event = new HashMap<>();
83+
event.put("event", "completed");
84+
eventSink.success(event);
85+
}
86+
87+
@Override
88+
public void onError(@NonNull String code, @Nullable String message, @Nullable Object details) {
89+
eventSink.error(code, message, details);
90+
}
91+
92+
@Override
93+
public void onIsPlayingStateUpdate(boolean isPlaying) {
94+
Map<String, Object> event = new HashMap<>();
95+
event.put("isPlaying", isPlaying);
96+
eventSink.success(event);
97+
}
98+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public void initialize() {
125125
player =
126126
new VideoPlayer(
127127
flutterState.applicationContext,
128-
eventChannel,
128+
VideoPlayerEventCallbacks.bindTo(eventChannel),
129129
handle,
130130
"asset:///" + assetLookupKey,
131131
null,
@@ -136,7 +136,7 @@ public void initialize() {
136136
player =
137137
new VideoPlayer(
138138
flutterState.applicationContext,
139-
eventChannel,
139+
VideoPlayerEventCallbacks.bindTo(eventChannel),
140140
handle,
141141
arg.getUri(),
142142
arg.getFormatHint(),

0 commit comments

Comments
 (0)