fix(video_player): buffering state events missing on Android & Web (fixes flutter/flutter#28494) (#2563)

diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md
index 14eaae3..e2bca40 100644
--- a/packages/video_player/video_player/CHANGELOG.md
+++ b/packages/video_player/video_player/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.0.0-nullsafety.4
+
+* Fixed an issue where `isBuffering` was not updating on Android.
+
 ## 2.0.0-nullsafety.3
 
 * Dart null safety requires `2.12`.
diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java
index 33c2f42..6565750 100644
--- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java
+++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java
@@ -169,10 +169,21 @@
 
     exoPlayer.addListener(
         new EventListener() {
+          private boolean isBuffering = false;
+
+          public void setBuffering(boolean buffering) {
+            if (isBuffering != buffering) {
+              isBuffering = buffering;
+              Map<String, Object> event = new HashMap<>();
+              event.put("event", isBuffering ? "bufferingStart" : "bufferingEnd");
+              eventSink.success(event);
+            }
+          }
 
           @Override
           public void onPlaybackStateChanged(final int playbackState) {
             if (playbackState == Player.STATE_BUFFERING) {
+              setBuffering(true);
               sendBufferingUpdate();
             } else if (playbackState == Player.STATE_READY) {
               if (!isInitialized) {
@@ -184,10 +195,15 @@
               event.put("event", "completed");
               eventSink.success(event);
             }
+
+            if (playbackState != Player.STATE_BUFFERING) {
+              setBuffering(false);
+            }
           }
 
           @Override
           public void onPlayerError(final ExoPlaybackException error) {
+            setBuffering(false);
             if (eventSink != null) {
               eventSink.error("VideoError", "Video player had error " + error, null);
             }
diff --git a/packages/video_player/video_player/example/integration_test/video_player_test.dart b/packages/video_player/video_player/example/integration_test/video_player_test.dart
index 7ef1cf6..9e273e0 100644
--- a/packages/video_player/video_player/example/integration_test/video_player_test.dart
+++ b/packages/video_player/video_player/example/integration_test/video_player_test.dart
@@ -2,11 +2,12 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-
 // TODO(amirh): Remove this once flutter_driver supports null safety.
 // https://github.com/flutter/flutter/issues/71379
 // @dart = 2.9
+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';
@@ -35,9 +36,57 @@
     });
 
     testWidgets(
+      'reports buffering status',
+      (WidgetTester tester) async {
+        VideoPlayerController networkController = VideoPlayerController.network(
+          'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
+        );
+        await networkController.initialize();
+        // Mute to allow playing without DOM interaction on Web.
+        // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
+        await networkController.setVolume(0);
+        final Completer<void> started = Completer();
+        final Completer<void> ended = Completer();
+        bool startedBuffering = false;
+        bool endedBuffering = false;
+        networkController.addListener(() {
+          if (networkController.value.isBuffering && !startedBuffering) {
+            startedBuffering = true;
+            started.complete();
+          }
+          if (startedBuffering &&
+              !networkController.value.isBuffering &&
+              !endedBuffering) {
+            endedBuffering = true;
+            ended.complete();
+          }
+        });
+
+        await networkController.play();
+        await networkController.seekTo(const Duration(seconds: 5));
+        await tester.pumpAndSettle(_playDuration);
+        await networkController.pause();
+
+        expect(networkController.value.isPlaying, false);
+        expect(networkController.value.position,
+            (Duration position) => position > const Duration(seconds: 0));
+
+        await started;
+        expect(startedBuffering, true);
+
+        await ended;
+        expect(endedBuffering, true);
+      },
+      skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android),
+    );
+
+    testWidgets(
       'can be played',
       (WidgetTester tester) async {
         await _controller.initialize();
+        // Mute to allow playing without DOM interaction on Web.
+        // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
+        await _controller.setVolume(0);
 
         await _controller.play();
         await tester.pumpAndSettle(_playDuration);
@@ -63,6 +112,9 @@
       'can be paused',
       (WidgetTester tester) async {
         await _controller.initialize();
+        // Mute to allow playing without DOM interaction on Web.
+        // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
+        await _controller.setVolume(0);
 
         // Play for a second, then pause, and then wait a second.
         await _controller.play();
@@ -109,6 +161,6 @@
 
       await tester.pumpAndSettle();
       expect(_controller.value.isPlaying, true);
-    });
+    }, skip: kIsWeb); // Web does not support local assets.
   });
 }
diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml
index cfbc4c6..6e48332 100644
--- a/packages/video_player/video_player/pubspec.yaml
+++ b/packages/video_player/video_player/pubspec.yaml
@@ -4,7 +4,7 @@
 # 0.10.y+z is compatible with 1.0.0, if you land a breaking change bump
 # the version to 2.0.0.
 # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0
-version: 2.0.0-nullsafety.3
+version: 2.0.0-nullsafety.4
 homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player
 
 flutter:
diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md
index 52c2042..2becf5b 100644
--- a/packages/video_player/video_player_web/CHANGELOG.md
+++ b/packages/video_player/video_player_web/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.0.0-nullsafety.2
+
+* Fixed an issue where `isBuffering` was not updating on Web.
+
 ## 2.0.0-nullsafety.1
 
 * Bump Dart SDK to support null safety.
diff --git a/packages/video_player/video_player_web/lib/video_player_web.dart b/packages/video_player/video_player_web/lib/video_player_web.dart
index de8f6a7..9132a08 100644
--- a/packages/video_player/video_player_web/lib/video_player_web.dart
+++ b/packages/video_player/video_player_web/lib/video_player_web.dart
@@ -156,6 +156,17 @@
   final int textureId;
   late VideoElement videoElement;
   bool isInitialized = false;
+  bool isBuffering = false;
+
+  void setBuffering(bool buffering) {
+    if (isBuffering != buffering) {
+      isBuffering = buffering;
+      eventController.add(VideoEvent(
+          eventType: isBuffering
+              ? VideoEventType.bufferingStart
+              : VideoEventType.bufferingEnd));
+    }
+  }
 
   void initialize() {
     videoElement = VideoElement()
@@ -176,10 +187,25 @@
         isInitialized = true;
         sendInitialized();
       }
+      setBuffering(false);
+    });
+
+    videoElement.onCanPlayThrough.listen((dynamic _) {
+      setBuffering(false);
+    });
+
+    videoElement.onPlaying.listen((dynamic _) {
+      setBuffering(false);
+    });
+
+    videoElement.onWaiting.listen((dynamic _) {
+      setBuffering(true);
+      sendBufferingUpdate();
     });
 
     // The error event fires when some form of error occurs while attempting to load or perform the media.
     videoElement.onError.listen((Event _) {
+      setBuffering(false);
       // The Event itself (_) doesn't contain info about the actual error.
       // We need to look at the HTMLMediaElement.error.
       // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error
@@ -192,6 +218,7 @@
     });
 
     videoElement.onEnded.listen((dynamic _) {
+      setBuffering(false);
       eventController.add(VideoEvent(eventType: VideoEventType.completed));
     });
   }
diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml
index dc1af16..9333ac0 100644
--- a/packages/video_player/video_player_web/pubspec.yaml
+++ b/packages/video_player/video_player_web/pubspec.yaml
@@ -4,7 +4,7 @@
 # 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump
 # the version to 2.0.0.
 # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0
-version: 2.0.0-nullsafety.1
+version: 2.0.0-nullsafety.2
 
 flutter:
   plugin: