Skip to content

Commit aeecebc

Browse files
authored
Background/resuming video_player on Android sends one initialized event (flutter#7722)
Closes flutter#154602. I also added small clarifications to the error in `video_player` (an assertion/error that are better than a `Completer.complete` default error) and updated the documentation in `video_player_platform` (it might have been obvious, but it definitely is now).
1 parent a566da6 commit aeecebc

File tree

11 files changed

+83
-9
lines changed

11 files changed

+83
-9
lines changed

packages/video_player/video_player/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
## NEXT
1+
## 2.9.2
22

33
* Updates minimum supported SDK version to Flutter 3.19/Dart 3.3.
4+
* Throws a more descriptive `StateError` in the case where
5+
`VideoPlayerController.initialize` receives more than one `initialized` event.
46

57
## 2.9.1
68

packages/video_player/video_player/lib/video_player.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,16 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
465465
errorDescription: null,
466466
isCompleted: false,
467467
);
468+
assert(
469+
!initializingCompleter.isCompleted,
470+
'VideoPlayerController already initialized. This is typically a '
471+
'sign that an implementation of the VideoPlayerPlatform '
472+
'(${_videoPlayerPlatform.runtimeType}) has a bug and is sending '
473+
'more than one initialized event per instance.',
474+
);
475+
if (initializingCompleter.isCompleted) {
476+
throw StateError('VideoPlayerController already initialized');
477+
}
468478
initializingCompleter.complete(null);
469479
_applyLooping();
470480
_applyVolume();

packages/video_player/video_player/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter
33
widgets on Android, iOS, and web.
44
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player
55
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
6-
version: 2.9.1
6+
version: 2.9.2
77

88
environment:
99
sdk: ^3.3.0

packages/video_player/video_player_android/CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.7.6
2+
3+
* Fixes a [bug](https://github.com/flutter/flutter/issues/154602) where
4+
resuming a video player would cause a `Bad state: Future already completed`.
5+
16
## 2.7.5
27

38
* Add a deprecation suppression in advance of a new `SurfaceProducer` API.
@@ -18,12 +23,10 @@
1823

1924
* Re-adds Impeller support.
2025

21-
2226
## 2.7.1
2327

2428
* Revert Impeller support.
2529

26-
2730
## 2.7.0
2831

2932
* Re-adds [support for Impeller](https://docs.flutter.dev/release/breaking-changes/android-surface-plugins).

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@ final class ExoPlayerEventListener implements Player.Listener {
1414
private final ExoPlayer exoPlayer;
1515
private final VideoPlayerCallbacks events;
1616
private boolean isBuffering = false;
17-
private boolean isInitialized = false;
17+
private boolean isInitialized;
1818

1919
ExoPlayerEventListener(ExoPlayer exoPlayer, VideoPlayerCallbacks events) {
20+
this(exoPlayer, events, false);
21+
}
22+
23+
ExoPlayerEventListener(ExoPlayer exoPlayer, VideoPlayerCallbacks events, boolean initialized) {
2024
this.exoPlayer = exoPlayer;
2125
this.events = events;
26+
this.isInitialized = initialized;
2227
}
2328

2429
private void setBuffering(boolean buffering) {

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ private ExoPlayer createVideoPlayer() {
108108
exoPlayer.prepare();
109109

110110
exoPlayer.setVideoSurface(surfaceProducer.getSurface());
111-
exoPlayer.addListener(new ExoPlayerEventListener(exoPlayer, videoPlayerEvents));
111+
112+
boolean wasInitialized = savedStateDuring != null;
113+
exoPlayer.addListener(new ExoPlayerEventListener(exoPlayer, videoPlayerEvents, wasInitialized));
112114
setAudioAttributes(exoPlayer, options.mixWithOthers);
113115

114116
return exoPlayer;

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import androidx.media3.common.C;
1414
import androidx.media3.common.PlaybackParameters;
1515
import androidx.media3.common.Player;
16+
import androidx.media3.common.VideoSize;
1617
import androidx.media3.exoplayer.ExoPlayer;
1718
import io.flutter.view.TextureRegistry;
1819
import org.junit.Before;
@@ -48,6 +49,7 @@ public final class VideoPlayerTest {
4849
@Mock private ExoPlayer mockExoPlayer;
4950
@Captor private ArgumentCaptor<AudioAttributes> attributesCaptor;
5051
@Captor private ArgumentCaptor<TextureRegistry.SurfaceProducer.Callback> callbackCaptor;
52+
@Captor private ArgumentCaptor<Player.Listener> listenerCaptor;
5153

5254
@Rule public MockitoRule initRule = MockitoJUnit.rule();
5355

@@ -197,6 +199,53 @@ public void onSurfaceProducerDestroyedAndRecreatedReleasesAndThenRecreatesAndRes
197199
videoPlayer.dispose();
198200
}
199201

202+
@Test
203+
public void onInitializedCalledWhenVideoPlayerInitiallyCreated() {
204+
VideoPlayer videoPlayer = createVideoPlayer();
205+
206+
// Pretend we have a video, and capture the registered event listener.
207+
when(mockExoPlayer.getVideoSize()).thenReturn(new VideoSize(300, 200));
208+
verify(mockExoPlayer).addListener(listenerCaptor.capture());
209+
Player.Listener listener = listenerCaptor.getValue();
210+
211+
// Trigger an event that would trigger onInitialized.
212+
listener.onPlaybackStateChanged(Player.STATE_READY);
213+
verify(mockEvents).onInitialized(anyInt(), anyInt(), anyLong(), anyInt());
214+
215+
videoPlayer.dispose();
216+
}
217+
218+
@Test
219+
public void onSurfaceCreatedDoesNotSendInitializeEventAgain() {
220+
// The VideoPlayer contract assumes that the event "initialized" is sent exactly once
221+
// (duplicate events cause an error to be thrown at the shared Dart layer). This test verifies
222+
// that the onInitialized event is sent exactly once per player.
223+
//
224+
// Regression test for https://github.com/flutter/flutter/issues/154602.
225+
VideoPlayer videoPlayer = createVideoPlayer();
226+
when(mockExoPlayer.getVideoSize()).thenReturn(new VideoSize(300, 200));
227+
228+
// Capture the lifecycle events so we can simulate onSurfaceCreated/Destroyed.
229+
verify(mockProducer).setCallback(callbackCaptor.capture());
230+
TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue();
231+
232+
// Trigger destroyed/created.
233+
producerLifecycle.onSurfaceDestroyed();
234+
producerLifecycle.onSurfaceCreated();
235+
236+
// Initial listener, and the new one from the resume.
237+
verify(mockExoPlayer, times(2)).addListener(listenerCaptor.capture());
238+
Player.Listener listener = listenerCaptor.getValue();
239+
240+
// Now trigger that same event, which would happen in the case of a background/resume.
241+
listener.onPlaybackStateChanged(Player.STATE_READY);
242+
243+
// Was not called because it was a result of a background/resume.
244+
verify(mockEvents, never()).onInitialized(anyInt(), anyInt(), anyLong(), anyInt());
245+
246+
videoPlayer.dispose();
247+
}
248+
200249
@Test
201250
public void onSurfaceCreatedWithoutDestroyDoesNotRecreate() {
202251
// Initially create the video player, which creates the initial surface.

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.7.5
5+
version: 2.7.6
66

77
environment:
88
sdk: ^3.5.0

packages/video_player/video_player_platform_interface/CHANGELOG.md

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

33
* Updates minimum supported SDK version to Flutter 3.19/Dart 3.3.
4+
* Clarified that `VideoEventType.initialized` cannot be sent more than once.
45

56
## 6.2.2
67

packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,8 @@ class VideoEvent {
278278
/// completed or to communicate buffering events or play state changed.
279279
enum VideoEventType {
280280
/// The video has been initialized.
281+
///
282+
/// A maximum of one event of this type may be emitted per instance.
281283
initialized,
282284

283285
/// The playback has ended.

packages/video_player/video_player_platform_interface/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/video_player/
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
55
# NOTE: We strongly prefer non-breaking changes, even at the expense of a
66
# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
7-
version: 6.2.2
7+
version: 6.2.3
88

99
environment:
1010
sdk: ^3.3.0

0 commit comments

Comments
 (0)