Skip to content

Commit dd04ab1

Browse files
authored
Move Player.Listener impl, remove @VisibleForTesting isInitialized. (flutter#6922)
Similar to flutter/packages#6908, as 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. In this PR, I've removed the concept of the class being "initialized" or not - the only thing "initialized" means is "for a given instance of `ExoPlayer`, has received the `'initialized'` event. As a result I removed the quasi-public API that was used for testing only and replaced it with observing what the real production instance does (`Player.STATE_READY`). After this PR, I'll likely do one more pass around the constructors - the constructor that takes an `ExoPlayer` that is marked `@VisibleForTesting` _also_ doesn't make sense once we'll support suspending/resuming video players, so it will need to get reworked (probably into taking a factory method).
1 parent a0f2552 commit dd04ab1

File tree

4 files changed

+167
-129
lines changed

4 files changed

+167
-129
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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.media3.common.PlaybackException;
9+
import androidx.media3.common.Player;
10+
import androidx.media3.common.VideoSize;
11+
import androidx.media3.exoplayer.ExoPlayer;
12+
13+
final class ExoPlayerEventListener implements Player.Listener {
14+
private final ExoPlayer exoPlayer;
15+
private final VideoPlayerCallbacks events;
16+
private boolean isBuffering = false;
17+
private boolean isInitialized = false;
18+
19+
ExoPlayerEventListener(ExoPlayer exoPlayer, VideoPlayerCallbacks events) {
20+
this.exoPlayer = exoPlayer;
21+
this.events = events;
22+
}
23+
24+
private void setBuffering(boolean buffering) {
25+
if (isBuffering == buffering) {
26+
return;
27+
}
28+
isBuffering = buffering;
29+
if (buffering) {
30+
events.onBufferingStart();
31+
} else {
32+
events.onBufferingEnd();
33+
}
34+
}
35+
36+
@SuppressWarnings("SuspiciousNameCombination")
37+
private void sendInitialized() {
38+
if (isInitialized) {
39+
return;
40+
}
41+
isInitialized = true;
42+
VideoSize videoSize = exoPlayer.getVideoSize();
43+
int rotationCorrection = 0;
44+
int width = videoSize.width;
45+
int height = videoSize.height;
46+
if (width != 0 && height != 0) {
47+
int rotationDegrees = videoSize.unappliedRotationDegrees;
48+
// Switch the width/height if video was taken in portrait mode
49+
if (rotationDegrees == 90 || rotationDegrees == 270) {
50+
width = videoSize.height;
51+
height = videoSize.width;
52+
}
53+
// Rotating the video with ExoPlayer does not seem to be possible with a Surface,
54+
// so inform the Flutter code that the widget needs to be rotated to prevent
55+
// upside-down playback for videos with rotationDegrees of 180 (other orientations work
56+
// correctly without correction).
57+
if (rotationDegrees == 180) {
58+
rotationCorrection = rotationDegrees;
59+
}
60+
}
61+
events.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection);
62+
}
63+
64+
@Override
65+
public void onPlaybackStateChanged(final int playbackState) {
66+
switch (playbackState) {
67+
case Player.STATE_BUFFERING:
68+
setBuffering(true);
69+
events.onBufferingUpdate(exoPlayer.getBufferedPosition());
70+
break;
71+
case Player.STATE_READY:
72+
sendInitialized();
73+
break;
74+
case Player.STATE_ENDED:
75+
events.onCompleted();
76+
break;
77+
case Player.STATE_IDLE:
78+
break;
79+
}
80+
if (playbackState != Player.STATE_BUFFERING) {
81+
setBuffering(false);
82+
}
83+
}
84+
85+
@Override
86+
public void onPlayerError(@NonNull final PlaybackException error) {
87+
setBuffering(false);
88+
if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {
89+
// See https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window
90+
exoPlayer.seekToDefaultPosition();
91+
exoPlayer.prepare();
92+
} else {
93+
events.onError("VideoError", "Video player had error " + error, null);
94+
}
95+
}
96+
97+
@Override
98+
public void onIsPlayingChanged(boolean isPlaying) {
99+
events.onIsPlayingStateUpdate(isPlaying);
100+
}
101+
}

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

+1-92
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,7 @@
1717
import androidx.media3.common.C;
1818
import androidx.media3.common.MediaItem;
1919
import androidx.media3.common.MimeTypes;
20-
import androidx.media3.common.PlaybackException;
2120
import androidx.media3.common.PlaybackParameters;
22-
import androidx.media3.common.Player;
23-
import androidx.media3.common.Player.Listener;
24-
import androidx.media3.common.VideoSize;
2521
import androidx.media3.common.util.UnstableApi;
2622
import androidx.media3.datasource.DataSource;
2723
import androidx.media3.datasource.DefaultDataSource;
@@ -47,8 +43,6 @@ final class VideoPlayer {
4743

4844
private static final String USER_AGENT = "User-Agent";
4945

50-
@VisibleForTesting boolean isInitialized = false;
51-
5246
private final VideoPlayerOptions options;
5347

5448
private final DefaultHttpDataSource.Factory httpDataSourceFactory;
@@ -116,61 +110,7 @@ private void setUpVideoPlayer(ExoPlayer exoPlayer) {
116110
surface = new Surface(textureEntry.surfaceTexture());
117111
exoPlayer.setVideoSurface(surface);
118112
setAudioAttributes(exoPlayer, options.mixWithOthers);
119-
120-
// Avoids synthetic accessor.
121-
VideoPlayerCallbacks events = this.videoPlayerEvents;
122-
123-
exoPlayer.addListener(
124-
new Listener() {
125-
private boolean isBuffering = false;
126-
127-
public void setBuffering(boolean buffering) {
128-
if (isBuffering == buffering) {
129-
return;
130-
}
131-
isBuffering = buffering;
132-
if (buffering) {
133-
events.onBufferingStart();
134-
} else {
135-
events.onBufferingEnd();
136-
}
137-
}
138-
139-
@Override
140-
public void onPlaybackStateChanged(final int playbackState) {
141-
if (playbackState == Player.STATE_BUFFERING) {
142-
setBuffering(true);
143-
sendBufferingUpdate();
144-
} else if (playbackState == Player.STATE_READY) {
145-
if (!isInitialized) {
146-
isInitialized = true;
147-
sendInitialized();
148-
}
149-
} else if (playbackState == Player.STATE_ENDED) {
150-
events.onCompleted();
151-
}
152-
if (playbackState != Player.STATE_BUFFERING) {
153-
setBuffering(false);
154-
}
155-
}
156-
157-
@Override
158-
public void onPlayerError(@NonNull final PlaybackException error) {
159-
setBuffering(false);
160-
if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {
161-
// See https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window
162-
exoPlayer.seekToDefaultPosition();
163-
exoPlayer.prepare();
164-
} else {
165-
events.onError("VideoError", "Video player had error " + error, null);
166-
}
167-
}
168-
169-
@Override
170-
public void onIsPlayingChanged(boolean isPlaying) {
171-
events.onIsPlayingStateUpdate(isPlaying);
172-
}
173-
});
113+
exoPlayer.addListener(new ExoPlayerEventListener(exoPlayer, videoPlayerEvents));
174114
}
175115

176116
void sendBufferingUpdate() {
@@ -216,38 +156,7 @@ long getPosition() {
216156
return exoPlayer.getCurrentPosition();
217157
}
218158

219-
@SuppressWarnings("SuspiciousNameCombination")
220-
@VisibleForTesting
221-
void sendInitialized() {
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;
242-
}
243-
}
244-
videoPlayerEvents.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection);
245-
}
246-
247159
void dispose() {
248-
if (isInitialized) {
249-
exoPlayer.stop();
250-
}
251160
textureEntry.release();
252161
if (surface != null) {
253162
surface.release();

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
import androidx.annotation.Nullable;
99
import androidx.annotation.VisibleForTesting;
1010
import io.flutter.plugin.common.EventChannel;
11+
import java.util.Arrays;
1112
import java.util.Collections;
1213
import java.util.HashMap;
14+
import java.util.List;
1315
import java.util.Map;
1416

1517
final class VideoPlayerEventCallbacks implements VideoPlayerCallbacks {
@@ -66,7 +68,10 @@ public void onBufferingStart() {
6668
public void onBufferingUpdate(long bufferedPosition) {
6769
// iOS supports a list of buffered ranges, so we send as a list with a single range.
6870
Map<String, Object> event = new HashMap<>();
69-
event.put("values", Collections.singletonList(bufferedPosition));
71+
event.put("event", "bufferingUpdate");
72+
73+
List<? extends Number> range = Arrays.asList(0, bufferedPosition);
74+
event.put("values", Collections.singletonList(range));
7075
eventSink.success(event);
7176
}
7277

0 commit comments

Comments
 (0)