[video_player] Fix a disposed `VideoPlayerController` throwing an exception when being replaced in the `VideoPlayer` (#4344)
* fix: do not removeListener if VideoPlayerController is already disposed
Co-authored-by: David Iglesias Teixeira <ditman@gmail.com>
diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md
index b0227fe..134b2dc 100644
--- a/packages/video_player/video_player/CHANGELOG.md
+++ b/packages/video_player/video_player/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.2.2
+
+* Fix a disposed `VideoPlayerController` throwing an exception when being replaced in the `VideoPlayer`.
+
## 2.2.1
* Specify Java 8 for Android build.
diff --git a/packages/video_player/video_player/example/integration_test/controller_swap_test.dart b/packages/video_player/video_player/example/integration_test/controller_swap_test.dart
new file mode 100644
index 0000000..cae5176
--- /dev/null
+++ b/packages/video_player/video_player/example/integration_test/controller_swap_test.dart
@@ -0,0 +1,92 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:video_player/video_player.dart';
+
+const Duration _playDuration = Duration(seconds: 1);
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+ testWidgets(
+ 'can substitute one controller by another without crashing',
+ (WidgetTester tester) async {
+ VideoPlayerController controller = VideoPlayerController.network(
+ 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
+ );
+ VideoPlayerController another = VideoPlayerController.network(
+ 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
+ );
+ await controller.initialize();
+ await another.initialize();
+ await controller.setVolume(0);
+ await another.setVolume(0);
+
+ final Completer<void> started = Completer();
+ final Completer<void> ended = Completer();
+ bool startedBuffering = false;
+ bool endedBuffering = false;
+
+ another.addListener(() {
+ if (another.value.isBuffering && !startedBuffering) {
+ startedBuffering = true;
+ started.complete();
+ }
+ if (startedBuffering && !another.value.isBuffering && !endedBuffering) {
+ endedBuffering = true;
+ ended.complete();
+ }
+ });
+
+ // Inject a widget with `controller`...
+ await tester.pumpWidget(renderVideoWidget(controller));
+ await controller.play();
+ await tester.pumpAndSettle(_playDuration);
+ await controller.pause();
+
+ // Disposing controller causes the Widget to crash in the next line
+ // (Issue https://github.com/flutter/flutter/issues/90046)
+ await controller.dispose();
+
+ // Now replace it with `another` controller...
+ await tester.pumpWidget(renderVideoWidget(another));
+ await another.play();
+ await another.seekTo(const Duration(seconds: 5));
+ await tester.pumpAndSettle(_playDuration);
+ await another.pause();
+
+ // Expect that `another` played.
+ expect(another.value.position,
+ (Duration position) => position > const Duration(seconds: 0));
+
+ await started;
+ expect(startedBuffering, true);
+
+ await ended;
+ expect(endedBuffering, true);
+ },
+ skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android),
+ );
+}
+
+Widget renderVideoWidget(VideoPlayerController controller) {
+ return Material(
+ elevation: 0,
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: AspectRatio(
+ key: Key('same'),
+ aspectRatio: controller.value.aspectRatio,
+ child: VideoPlayer(controller),
+ ),
+ ),
+ ),
+ );
+}
diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart
index 685563a..0d682c9 100644
--- a/packages/video_player/video_player/lib/video_player.dart
+++ b/packages/video_player/video_player/lib/video_player.dart
@@ -594,6 +594,15 @@
value = value.copyWith(caption: _getCaptionAt(position));
}
+ @override
+ void removeListener(VoidCallback listener) {
+ // Prevent VideoPlayer from causing an exception to be thrown when attempting to
+ // remove its own listener after the controller has already been disposed.
+ if (!_isDisposed) {
+ super.removeListener(listener);
+ }
+ }
+
bool get _isDisposedOrNotInitialized => _isDisposed || !value.isInitialized;
}
diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml
index 658357b..2fa5d66 100644
--- a/packages/video_player/video_player/pubspec.yaml
+++ b/packages/video_player/video_player/pubspec.yaml
@@ -3,7 +3,7 @@
widgets on Android, iOS, and web.
repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
-version: 2.2.1
+version: 2.2.2
environment:
sdk: ">=2.12.0 <3.0.0"