[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"