[video_player] Eliminate platform channel from mock platform (#4588)

diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md
index 9d4e36d..6f2a2b7 100644
--- a/packages/video_player/video_player/CHANGELOG.md
+++ b/packages/video_player/video_player/CHANGELOG.md
@@ -1,5 +1,8 @@
-## NEXT
+## 2.2.8
 
+* Changes the way the `VideoPlayerPlatform` instance is cached in the
+  controller, so that it's no longer impossible to change after the first use.
+* Updates unit tests to be self-contained.
 * Fixes integration tests.
 * Updates Android compileSdkVersion to 31.
 * Fixes a flaky integration test.
diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart
index ff727f1..523a1ad 100644
--- a/packages/video_player/video_player/lib/video_player.dart
+++ b/packages/video_player/video_player/lib/video_player.dart
@@ -17,10 +17,18 @@
 import 'src/closed_caption_file.dart';
 export 'src/closed_caption_file.dart';
 
-final VideoPlayerPlatform _videoPlayerPlatform = VideoPlayerPlatform.instance
-  // This will clear all open videos on the platform when a full restart is
-  // performed.
-  ..init();
+VideoPlayerPlatform? _lastVideoPlayerPlatform;
+
+VideoPlayerPlatform get _videoPlayerPlatform {
+  VideoPlayerPlatform currentInstance = VideoPlayerPlatform.instance;
+  if (_lastVideoPlayerPlatform != currentInstance) {
+    // This will clear all open videos on the platform when a full restart is
+    // performed.
+    currentInstance.init();
+    _lastVideoPlayerPlatform = currentInstance;
+  }
+  return currentInstance;
+}
 
 /// The duration, current position, buffering state, error state and settings
 /// of a [VideoPlayerController].
diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml
index f0d5951..f8c0917 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.7
+version: 2.2.8
 
 environment:
   sdk: ">=2.14.0 <3.0.0"
@@ -25,12 +25,6 @@
     sdk: flutter
   meta: ^1.3.0
   video_player_platform_interface: ^4.2.0
-  # The design on https://flutter.dev/go/federated-plugins was to leave
-  # this constraint as "any". We cannot do it right now as it fails pub publish
-  # validation, so we set a ^ constraint. The exact value doesn't matter since
-  # the constraints on the interface pins it.
-  # TODO(amirh): Revisit this (either update this part in the design or the pub tool).
-  # https://github.com/flutter/flutter/issues/46264
   video_player_web: ^2.0.0
   html: ^0.15.0
 
diff --git a/packages/video_player/video_player/test/video_player_initialization_test.dart b/packages/video_player/video_player/test/video_player_initialization_test.dart
index 13bfd7b..1870934 100644
--- a/packages/video_player/video_player/test/video_player_initialization_test.dart
+++ b/packages/video_player/video_player/test/video_player_initialization_test.dart
@@ -4,6 +4,7 @@
 
 import 'package:flutter_test/flutter_test.dart';
 import 'package:video_player/video_player.dart';
+import 'package:video_player_platform_interface/video_player_platform_interface.dart';
 
 import 'video_player_test.dart' show FakeVideoPlayerPlatform;
 
@@ -13,6 +14,7 @@
   test('plugin initialized', () async {
     TestWidgetsFlutterBinding.ensureInitialized();
     FakeVideoPlayerPlatform fakeVideoPlayerPlatform = FakeVideoPlayerPlatform();
+    VideoPlayerPlatform.instance = fakeVideoPlayerPlatform;
 
     final VideoPlayerController controller = VideoPlayerController.network(
       'https://127.0.0.1',
diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart
index 08fd9dc..959f98f 100644
--- a/packages/video_player/video_player/test/video_player_test.dart
+++ b/packages/video_player/video_player/test/video_player_test.dart
@@ -11,8 +11,6 @@
 import 'package:flutter/widgets.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:video_player/video_player.dart';
-import 'package:video_player_platform_interface/messages.dart';
-import 'package:video_player_platform_interface/test.dart';
 import 'package:video_player_platform_interface/video_player_platform_interface.dart';
 
 class FakeController extends ValueNotifier<VideoPlayerValue>
@@ -187,6 +185,7 @@
 
     setUp(() {
       fakeVideoPlayerPlatform = FakeVideoPlayerPlatform();
+      VideoPlayerPlatform.instance = fakeVideoPlayerPlatform;
     });
 
     group('initialize', () {
@@ -196,10 +195,8 @@
         );
         await controller.initialize();
 
-        expect(
-            fakeVideoPlayerPlatform.dataSourceDescriptions[0].asset, 'a.avi');
-        expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].packageName,
-            null);
+        expect(fakeVideoPlayerPlatform.dataSources[0].asset, 'a.avi');
+        expect(fakeVideoPlayerPlatform.dataSources[0].package, null);
       });
 
       test('network', () async {
@@ -209,15 +206,15 @@
         await controller.initialize();
 
         expect(
-          fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri,
+          fakeVideoPlayerPlatform.dataSources[0].uri,
           'https://127.0.0.1',
         );
         expect(
-          fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint,
+          fakeVideoPlayerPlatform.dataSources[0].formatHint,
           null,
         );
         expect(
-          fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders,
+          fakeVideoPlayerPlatform.dataSources[0].httpHeaders,
           {},
         );
       });
@@ -230,16 +227,16 @@
         await controller.initialize();
 
         expect(
-          fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri,
+          fakeVideoPlayerPlatform.dataSources[0].uri,
           'https://127.0.0.1',
         );
         expect(
-          fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint,
-          'dash',
+          fakeVideoPlayerPlatform.dataSources[0].formatHint,
+          VideoFormat.dash,
         );
         expect(
-          fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders,
-          {},
+          fakeVideoPlayerPlatform.dataSources[0].httpHeaders,
+          <String, String>{},
         );
       });
 
@@ -251,15 +248,15 @@
         await controller.initialize();
 
         expect(
-          fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri,
+          fakeVideoPlayerPlatform.dataSources[0].uri,
           'https://127.0.0.1',
         );
         expect(
-          fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint,
+          fakeVideoPlayerPlatform.dataSources[0].formatHint,
           null,
         );
         expect(
-          fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders,
+          fakeVideoPlayerPlatform.dataSources[0].httpHeaders,
           {'Authorization': 'Bearer token'},
         );
       });
@@ -268,15 +265,12 @@
         final VideoPlayerController controller = VideoPlayerController.network(
           'http://testing.com/invalid_url',
         );
-        try {
-          late dynamic error;
-          fakeVideoPlayerPlatform.forceInitError = true;
-          await controller.initialize().catchError((dynamic e) => error = e);
-          final PlatformException platformEx = error;
-          expect(platformEx.code, equals('VideoError'));
-        } finally {
-          fakeVideoPlayerPlatform.forceInitError = false;
-        }
+
+        late dynamic error;
+        fakeVideoPlayerPlatform.forceInitError = true;
+        await controller.initialize().catchError((dynamic e) => error = e);
+        final PlatformException platformEx = error;
+        expect(platformEx.code, equals('VideoError'));
       });
 
       test('file', () async {
@@ -284,8 +278,7 @@
             VideoPlayerController.file(File('a.avi'));
         await controller.initialize();
 
-        expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri,
-            'file://a.avi');
+        expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'file://a.avi');
       });
     });
 
@@ -294,8 +287,7 @@
           VideoPlayerController.contentUri(Uri.parse('content://video'));
       await controller.initialize();
 
-      expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri,
-          'content://video');
+      expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'content://video');
     });
 
     test('dispose', () async {
@@ -571,11 +563,11 @@
         expect(controller.value.isPlaying, isFalse);
         await controller.play();
         expect(controller.value.isPlaying, isTrue);
-        final FakeVideoEventStream fakeVideoEventStream =
+        final StreamController<VideoEvent> fakeVideoEventStream =
             fakeVideoPlayerPlatform.streams[controller.textureId]!;
 
-        fakeVideoEventStream.eventsChannel
-            .sendEvent(<String, dynamic>{'event': 'completed'});
+        fakeVideoEventStream
+            .add(VideoEvent(eventType: VideoEventType.completed));
         await tester.pumpAndSettle();
 
         expect(controller.value.isPlaying, isFalse);
@@ -589,30 +581,30 @@
         await controller.initialize();
         expect(controller.value.isBuffering, false);
         expect(controller.value.buffered, isEmpty);
-        final FakeVideoEventStream fakeVideoEventStream =
+        final StreamController<VideoEvent> fakeVideoEventStream =
             fakeVideoPlayerPlatform.streams[controller.textureId]!;
 
-        fakeVideoEventStream.eventsChannel
-            .sendEvent(<String, dynamic>{'event': 'bufferingStart'});
+        fakeVideoEventStream
+            .add(VideoEvent(eventType: VideoEventType.bufferingStart));
         await tester.pumpAndSettle();
         expect(controller.value.isBuffering, isTrue);
 
         const Duration bufferStart = Duration(seconds: 0);
         const Duration bufferEnd = Duration(milliseconds: 500);
-        fakeVideoEventStream.eventsChannel.sendEvent(<String, dynamic>{
-          'event': 'bufferingUpdate',
-          'values': <List<int>>[
-            <int>[bufferStart.inMilliseconds, bufferEnd.inMilliseconds]
-          ],
-        });
+        fakeVideoEventStream
+          ..add(VideoEvent(
+              eventType: VideoEventType.bufferingUpdate,
+              buffered: <DurationRange>[
+                DurationRange(bufferStart, bufferEnd),
+              ]));
         await tester.pumpAndSettle();
         expect(controller.value.isBuffering, isTrue);
         expect(controller.value.buffered.length, 1);
         expect(controller.value.buffered[0].toString(),
             DurationRange(bufferStart, bufferEnd).toString());
 
-        fakeVideoEventStream.eventsChannel
-            .sendEvent(<String, dynamic>{'event': 'bufferingEnd'});
+        fakeVideoEventStream
+            .add(VideoEvent(eventType: VideoEventType.bufferingEnd));
         await tester.pumpAndSettle();
         expect(controller.value.isBuffering, isFalse);
       });
@@ -807,155 +799,88 @@
   });
 }
 
-class FakeVideoPlayerPlatform extends TestHostVideoPlayerApi {
-  FakeVideoPlayerPlatform() {
-    TestHostVideoPlayerApi.setup(this);
-  }
-
+class FakeVideoPlayerPlatform extends VideoPlayerPlatform {
   Completer<bool> initialized = Completer<bool>();
   List<String> calls = <String>[];
-  List<CreateMessage> dataSourceDescriptions = <CreateMessage>[];
-  final Map<int, FakeVideoEventStream> streams = <int, FakeVideoEventStream>{};
+  List<DataSource> dataSources = <DataSource>[];
+  final Map<int, StreamController<VideoEvent>> streams =
+      <int, StreamController<VideoEvent>>{};
   bool forceInitError = false;
   int nextTextureId = 0;
   final Map<int, Duration> _positions = <int, Duration>{};
 
   @override
-  TextureMessage create(CreateMessage arg) {
+  Future<int?> create(DataSource dataSource) async {
     calls.add('create');
-    streams[nextTextureId] = FakeVideoEventStream(
-        nextTextureId, 100, 100, const Duration(seconds: 1), forceInitError);
-    TextureMessage result = TextureMessage();
-    result.textureId = nextTextureId++;
-    dataSourceDescriptions.add(arg);
-    return result;
+    StreamController<VideoEvent> stream = StreamController<VideoEvent>();
+    streams[nextTextureId] = stream;
+    if (forceInitError) {
+      stream.addError(PlatformException(
+          code: 'VideoError', message: 'Video player had error XYZ'));
+    } else {
+      stream.add(VideoEvent(
+          eventType: VideoEventType.initialized,
+          size: Size(100, 100),
+          duration: Duration(seconds: 1)));
+    }
+    dataSources.add(dataSource);
+    return nextTextureId++;
   }
 
   @override
-  void dispose(TextureMessage arg) {
+  Future<void> dispose(int textureId) async {
     calls.add('dispose');
   }
 
   @override
-  void initialize() {
+  Future<void> init() async {
     calls.add('init');
     initialized.complete(true);
   }
 
+  Stream<VideoEvent> videoEventsFor(int textureId) {
+    return streams[textureId]!.stream;
+  }
+
   @override
-  void pause(TextureMessage arg) {
+  Future<void> pause(int textureId) async {
     calls.add('pause');
   }
 
   @override
-  void play(TextureMessage arg) {
+  Future<void> play(int textureId) async {
     calls.add('play');
   }
 
   @override
-  PositionMessage position(TextureMessage arg) {
+  Future<Duration> getPosition(int textureId) async {
     calls.add('position');
-    final Duration position =
-        _positions[arg.textureId] ?? const Duration(seconds: 0);
-    return PositionMessage()..position = position.inMilliseconds;
+    return _positions[textureId] ?? const Duration(seconds: 0);
   }
 
   @override
-  void seekTo(PositionMessage arg) {
+  Future<void> seekTo(int textureId, Duration position) async {
     calls.add('seekTo');
-    _positions[arg.textureId!] = Duration(milliseconds: arg.position!);
+    _positions[textureId] = position;
   }
 
   @override
-  void setLooping(LoopingMessage arg) {
+  Future<void> setLooping(int textureId, bool looping) async {
     calls.add('setLooping');
   }
 
   @override
-  void setVolume(VolumeMessage arg) {
+  Future<void> setVolume(int textureId, double volume) async {
     calls.add('setVolume');
   }
 
   @override
-  void setPlaybackSpeed(PlaybackSpeedMessage arg) {
+  Future<void> setPlaybackSpeed(int textureId, double speed) async {
     calls.add('setPlaybackSpeed');
   }
 
   @override
-  void setMixWithOthers(MixWithOthersMessage arg) {
+  Future<void> setMixWithOthers(bool mixWithOthers) async {
     calls.add('setMixWithOthers');
   }
 }
-
-class FakeVideoEventStream {
-  FakeVideoEventStream(this.textureId, this.width, this.height, this.duration,
-      this.initWithError) {
-    eventsChannel = FakeEventsChannel(
-        'flutter.io/videoPlayer/videoEvents$textureId', onListen);
-  }
-
-  int textureId;
-  int width;
-  int height;
-  Duration duration;
-  bool initWithError;
-  late FakeEventsChannel eventsChannel;
-
-  void onListen() {
-    if (!initWithError) {
-      eventsChannel.sendEvent(<String, dynamic>{
-        'event': 'initialized',
-        'duration': duration.inMilliseconds,
-        'width': width,
-        'height': height,
-      });
-    } else {
-      eventsChannel.sendError('VideoError', 'Video player had error XYZ');
-    }
-  }
-}
-
-class FakeEventsChannel {
-  FakeEventsChannel(String name, this.onListen) {
-    eventsMethodChannel = MethodChannel(name);
-    eventsMethodChannel.setMockMethodCallHandler(onMethodCall);
-  }
-
-  late MethodChannel eventsMethodChannel;
-  VoidCallback onListen;
-
-  Future<dynamic> onMethodCall(MethodCall call) {
-    switch (call.method) {
-      case 'listen':
-        onListen();
-        break;
-    }
-    return Future<void>.sync(() {});
-  }
-
-  void sendEvent(dynamic event) {
-    _sendMessage(const StandardMethodCodec().encodeSuccessEnvelope(event));
-  }
-
-  void sendError(String code, [String? message, dynamic details]) {
-    _sendMessage(const StandardMethodCodec().encodeErrorEnvelope(
-      code: code,
-      message: message,
-      details: details,
-    ));
-  }
-
-  void _sendMessage(ByteData data) {
-    _ambiguate(ServicesBinding.instance)!
-        .defaultBinaryMessenger
-        .handlePlatformMessage(
-            eventsMethodChannel.name, data, (ByteData? data) {});
-  }
-}
-
-/// This allows a value of type T or T? to be treated as a value of type T?.
-///
-/// We use this so that APIs that have become non-nullable can still be used
-/// with `!` and `?` on the stable branch.
-// TODO(ianh): Remove this once we roll stable in late 2021.
-T? _ambiguate<T>(T? value) => value;