[video_player] synchronize isPlaying state (#3261)

[video_player] synchronize isPlaying state
diff --git a/packages/video_player/video_player/AUTHORS b/packages/video_player/video_player/AUTHORS
index 02a9c69..e57e00b 100644
--- a/packages/video_player/video_player/AUTHORS
+++ b/packages/video_player/video_player/AUTHORS
@@ -65,3 +65,4 @@
 Alex Li <google@alexv525.com>
 Rahul Raj <64.rahulraj@gmail.com>
 Koen Van Looveren <vanlooverenkoen.dev@gmail.com>
+Márton Matuz <matuzmarci@gmail.com>
diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md
index 67c9fc4..368af5b 100644
--- a/packages/video_player/video_player/CHANGELOG.md
+++ b/packages/video_player/video_player/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.6.1
+
+* Synchronizes `VideoPlayerValue.isPlaying` with underlying video player.
+
 ## 2.6.0
 
 * Adds option to configure HTTP headers via `VideoPlayerController` to fix access to M3U8 files on Android.
diff --git a/packages/video_player/video_player/lib/src/closed_caption_file.dart b/packages/video_player/video_player/lib/src/closed_caption_file.dart
index 324ffc4..91b4468 100644
--- a/packages/video_player/video_player/lib/src/closed_caption_file.dart
+++ b/packages/video_player/video_player/lib/src/closed_caption_file.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:flutter/foundation.dart' show objectRuntimeType;
+import 'package:flutter/foundation.dart' show immutable, objectRuntimeType;
 
 import 'sub_rip.dart';
 import 'web_vtt.dart';
@@ -32,6 +32,7 @@
 ///
 /// A typical closed captioning file will include several [Caption]s, each
 /// linked to a start and end time.
+@immutable
 class Caption {
   /// Creates a new [Caption] object.
   ///
@@ -74,4 +75,22 @@
         'end: $end, '
         'text: $text)';
   }
+
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      other is Caption &&
+          runtimeType == other.runtimeType &&
+          number == other.number &&
+          start == other.start &&
+          end == other.end &&
+          text == other.text;
+
+  @override
+  int get hashCode => Object.hash(
+        number,
+        start,
+        end,
+        text,
+      );
 }
diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart
index 6a7661f..b2105e9 100644
--- a/packages/video_player/video_player/lib/video_player.dart
+++ b/packages/video_player/video_player/lib/video_player.dart
@@ -33,10 +33,11 @@
 
 /// The duration, current position, buffering state, error state and settings
 /// of a [VideoPlayerController].
+@immutable
 class VideoPlayerValue {
   /// Constructs a video with the given values. Only [duration] is required. The
   /// rest will initialize with default values when unset.
-  VideoPlayerValue({
+  const VideoPlayerValue({
     required this.duration,
     this.size = Size.zero,
     this.position = Duration.zero,
@@ -54,11 +55,11 @@
   });
 
   /// Returns an instance for a video that hasn't been loaded.
-  VideoPlayerValue.uninitialized()
+  const VideoPlayerValue.uninitialized()
       : this(duration: Duration.zero, isInitialized: false);
 
   /// Returns an instance with the given [errorDescription].
-  VideoPlayerValue.erroneous(String errorDescription)
+  const VideoPlayerValue.erroneous(String errorDescription)
       : this(
             duration: Duration.zero,
             isInitialized: false,
@@ -195,6 +196,44 @@
         'playbackSpeed: $playbackSpeed, '
         'errorDescription: $errorDescription)';
   }
+
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      other is VideoPlayerValue &&
+          runtimeType == other.runtimeType &&
+          duration == other.duration &&
+          position == other.position &&
+          caption == other.caption &&
+          captionOffset == other.captionOffset &&
+          listEquals(buffered, other.buffered) &&
+          isPlaying == other.isPlaying &&
+          isLooping == other.isLooping &&
+          isBuffering == other.isBuffering &&
+          volume == other.volume &&
+          playbackSpeed == other.playbackSpeed &&
+          errorDescription == other.errorDescription &&
+          size == other.size &&
+          rotationCorrection == other.rotationCorrection &&
+          isInitialized == other.isInitialized;
+
+  @override
+  int get hashCode => Object.hash(
+        duration,
+        position,
+        caption,
+        captionOffset,
+        buffered,
+        isPlaying,
+        isLooping,
+        isBuffering,
+        volume,
+        playbackSpeed,
+        errorDescription,
+        size,
+        rotationCorrection,
+        isInitialized,
+      );
 }
 
 /// Controls a platform video player, and provides updates when the state is
@@ -221,7 +260,7 @@
         dataSourceType = DataSourceType.asset,
         formatHint = null,
         httpHeaders = const <String, String>{},
-        super(VideoPlayerValue(duration: Duration.zero));
+        super(const VideoPlayerValue(duration: Duration.zero));
 
   /// Constructs a [VideoPlayerController] playing a video from obtained from
   /// the network.
@@ -241,7 +280,7 @@
   })  : _closedCaptionFileFuture = closedCaptionFile,
         dataSourceType = DataSourceType.network,
         package = null,
-        super(VideoPlayerValue(duration: Duration.zero));
+        super(const VideoPlayerValue(duration: Duration.zero));
 
   /// Constructs a [VideoPlayerController] playing a video from a file.
   ///
@@ -256,7 +295,7 @@
         dataSourceType = DataSourceType.file,
         package = null,
         formatHint = null,
-        super(VideoPlayerValue(duration: Duration.zero));
+        super(const VideoPlayerValue(duration: Duration.zero));
 
   /// Constructs a [VideoPlayerController] playing a video from a contentUri.
   ///
@@ -272,7 +311,7 @@
         package = null,
         formatHint = null,
         httpHeaders = const <String, String>{},
-        super(VideoPlayerValue(duration: Duration.zero));
+        super(const VideoPlayerValue(duration: Duration.zero));
 
   /// The URI to the video file. This will be in different formats depending on
   /// the [DataSourceType] of the original video.
@@ -372,7 +411,6 @@
         return;
       }
 
-      // ignore: missing_enum_constant_in_switch
       switch (event.eventType) {
         case VideoEventType.initialized:
           value = value.copyWith(
@@ -403,6 +441,9 @@
         case VideoEventType.bufferingEnd:
           value = value.copyWith(isBuffering: false);
           break;
+        case VideoEventType.isPlayingStateUpdate:
+          value = value.copyWith(isPlaying: event.isPlaying);
+          break;
         case VideoEventType.unknown:
           break;
       }
diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml
index ffe90c5..91d0a2c 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/packages/tree/main/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.6.0
+version: 2.6.1
 
 environment:
   sdk: ">=2.17.0 <4.0.0"
@@ -25,7 +25,7 @@
   html: ^0.15.0
   video_player_android: ^2.3.5
   video_player_avfoundation: ^2.2.17
-  video_player_platform_interface: ">=5.1.1 <7.0.0"
+  video_player_platform_interface: ">=6.1.0 <7.0.0"
   video_player_web: ^2.0.0
 
 dev_dependencies:
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 a24ac84..ef4f0bf 100644
--- a/packages/video_player/video_player/test/video_player_test.dart
+++ b/packages/video_player/video_player/test/video_player_test.dart
@@ -16,7 +16,7 @@
 
 class FakeController extends ValueNotifier<VideoPlayerValue>
     implements VideoPlayerController {
-  FakeController() : super(VideoPlayerValue(duration: Duration.zero));
+  FakeController() : super(const VideoPlayerValue(duration: Duration.zero));
 
   FakeController.value(super.value);
 
@@ -164,7 +164,8 @@
   testWidgets('non-zero rotationCorrection value is used',
       (WidgetTester tester) async {
     final FakeController controller = FakeController.value(
-        VideoPlayerValue(duration: Duration.zero, rotationCorrection: 180));
+        const VideoPlayerValue(
+            duration: Duration.zero, rotationCorrection: 180));
     controller.textureId = 1;
     await tester.pumpWidget(VideoPlayer(controller));
     final Transform actualRotationCorrection =
@@ -184,7 +185,7 @@
   testWidgets('no transform when rotationCorrection is zero',
       (WidgetTester tester) async {
     final FakeController controller =
-        FakeController.value(VideoPlayerValue(duration: Duration.zero));
+        FakeController.value(const VideoPlayerValue(duration: Duration.zero));
     controller.textureId = 1;
     await tester.pumpWidget(VideoPlayer(controller));
     expect(find.byType(Transform), findsNothing);
@@ -803,6 +804,30 @@
         expect(controller.value.position, nonzeroDuration);
       });
 
+      testWidgets('playback status', (WidgetTester tester) async {
+        final VideoPlayerController controller = VideoPlayerController.network(
+          'https://127.0.0.1',
+        );
+        await controller.initialize();
+        expect(controller.value.isPlaying, isFalse);
+        final StreamController<VideoEvent> fakeVideoEventStream =
+            fakeVideoPlayerPlatform.streams[controller.textureId]!;
+
+        fakeVideoEventStream.add(VideoEvent(
+          eventType: VideoEventType.isPlayingStateUpdate,
+          isPlaying: true,
+        ));
+        await tester.pumpAndSettle();
+        expect(controller.value.isPlaying, isTrue);
+
+        fakeVideoEventStream.add(VideoEvent(
+          eventType: VideoEventType.isPlayingStateUpdate,
+          isPlaying: false,
+        ));
+        await tester.pumpAndSettle();
+        expect(controller.value.isPlaying, isFalse);
+      });
+
       testWidgets('buffering status', (WidgetTester tester) async {
         final VideoPlayerController controller = VideoPlayerController.network(
           'https://127.0.0.1',
@@ -865,7 +890,7 @@
 
   group('VideoPlayerValue', () {
     test('uninitialized()', () {
-      final VideoPlayerValue uninitialized = VideoPlayerValue.uninitialized();
+      const VideoPlayerValue uninitialized = VideoPlayerValue.uninitialized();
 
       expect(uninitialized.duration, equals(Duration.zero));
       expect(uninitialized.position, equals(Duration.zero));
@@ -886,7 +911,7 @@
 
     test('erroneous()', () {
       const String errorMessage = 'foo';
-      final VideoPlayerValue error = VideoPlayerValue.erroneous(errorMessage);
+      const VideoPlayerValue error = VideoPlayerValue.erroneous(errorMessage);
 
       expect(error.duration, equals(Duration.zero));
       expect(error.position, equals(Duration.zero));
@@ -956,26 +981,26 @@
 
     group('copyWith()', () {
       test('exact copy', () {
-        final VideoPlayerValue original = VideoPlayerValue.uninitialized();
+        const VideoPlayerValue original = VideoPlayerValue.uninitialized();
         final VideoPlayerValue exactCopy = original.copyWith();
 
         expect(exactCopy.toString(), original.toString());
       });
       test('errorDescription is not persisted when copy with null', () {
-        final VideoPlayerValue original = VideoPlayerValue.erroneous('error');
+        const VideoPlayerValue original = VideoPlayerValue.erroneous('error');
         final VideoPlayerValue copy = original.copyWith(errorDescription: null);
 
         expect(copy.errorDescription, null);
       });
       test('errorDescription is changed when copy with another error', () {
-        final VideoPlayerValue original = VideoPlayerValue.erroneous('error');
+        const VideoPlayerValue original = VideoPlayerValue.erroneous('error');
         final VideoPlayerValue copy =
             original.copyWith(errorDescription: 'new error');
 
         expect(copy.errorDescription, 'new error');
       });
       test('errorDescription is changed when copy with error', () {
-        final VideoPlayerValue original = VideoPlayerValue.uninitialized();
+        const VideoPlayerValue original = VideoPlayerValue.uninitialized();
         final VideoPlayerValue copy =
             original.copyWith(errorDescription: 'new error');
 
@@ -985,45 +1010,45 @@
 
     group('aspectRatio', () {
       test('640x480 -> 4:3', () {
-        final VideoPlayerValue value = VideoPlayerValue(
+        const VideoPlayerValue value = VideoPlayerValue(
           isInitialized: true,
-          size: const Size(640, 480),
-          duration: const Duration(seconds: 1),
+          size: Size(640, 480),
+          duration: Duration(seconds: 1),
         );
         expect(value.aspectRatio, 4 / 3);
       });
 
       test('no size -> 1.0', () {
-        final VideoPlayerValue value = VideoPlayerValue(
+        const VideoPlayerValue value = VideoPlayerValue(
           isInitialized: true,
-          duration: const Duration(seconds: 1),
+          duration: Duration(seconds: 1),
         );
         expect(value.aspectRatio, 1.0);
       });
 
       test('height = 0 -> 1.0', () {
-        final VideoPlayerValue value = VideoPlayerValue(
+        const VideoPlayerValue value = VideoPlayerValue(
           isInitialized: true,
-          size: const Size(640, 0),
-          duration: const Duration(seconds: 1),
+          size: Size(640, 0),
+          duration: Duration(seconds: 1),
         );
         expect(value.aspectRatio, 1.0);
       });
 
       test('width = 0 -> 1.0', () {
-        final VideoPlayerValue value = VideoPlayerValue(
+        const VideoPlayerValue value = VideoPlayerValue(
           isInitialized: true,
-          size: const Size(0, 480),
-          duration: const Duration(seconds: 1),
+          size: Size(0, 480),
+          duration: Duration(seconds: 1),
         );
         expect(value.aspectRatio, 1.0);
       });
 
       test('negative aspect ratio -> 1.0', () {
-        final VideoPlayerValue value = VideoPlayerValue(
+        const VideoPlayerValue value = VideoPlayerValue(
           isInitialized: true,
-          size: const Size(640, -480),
-          duration: const Duration(seconds: 1),
+          size: Size(640, -480),
+          duration: Duration(seconds: 1),
         );
         expect(value.aspectRatio, 1.0);
       });
diff --git a/packages/video_player/video_player_android/AUTHORS b/packages/video_player/video_player_android/AUTHORS
index 493a0b4..fc16c35 100644
--- a/packages/video_player/video_player_android/AUTHORS
+++ b/packages/video_player/video_player_android/AUTHORS
@@ -64,3 +64,4 @@
 Anton Borries <mail@antonborri.es>
 Alex Li <google@alexv525.com>
 Rahul Raj <64.rahulraj@gmail.com>
+Márton Matuz <matuzmarci@gmail.com>
diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md
index fcb4f98..7dbfe5d 100644
--- a/packages/video_player/video_player_android/CHANGELOG.md
+++ b/packages/video_player/video_player_android/CHANGELOG.md
@@ -1,5 +1,6 @@
-## NEXT
+## 2.4.4
 
+* Synchronizes `VideoPlayerValue.isPlaying` with `ExoPlayer`.
 * Updates minimum Flutter version to 3.3.
 
 ## 2.4.3
diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java
index 2aed171..4701c79 100644
--- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java
+++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java
@@ -231,6 +231,16 @@
               eventSink.error("VideoError", "Video player had error " + error, null);
             }
           }
+
+          @Override
+          public void onIsPlayingChanged(boolean isPlaying) {
+            if (eventSink != null) {
+              Map<String, Object> event = new HashMap<>();
+              event.put("event", "isPlayingStateUpdate");
+              event.put("isPlaying", isPlaying);
+              eventSink.success(event);
+            }
+          }
         });
   }
 
diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java
index 5a646d8..2ba6ee0 100644
--- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java
+++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java
@@ -5,10 +5,13 @@
 package io.flutter.plugins.videoplayer;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -25,6 +28,7 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
 import org.robolectric.RobolectricTestRunner;
 
 @RunWith(RobolectricTestRunner.class)
@@ -234,4 +238,44 @@
     assertEquals(event.get("height"), 200);
     assertEquals(event.get("rotationCorrection"), 180);
   }
+
+  @Test
+  public void onIsPlayingChangedSendsExpectedEvent() {
+    VideoPlayer videoPlayer =
+        new VideoPlayer(
+            fakeExoPlayer,
+            fakeEventChannel,
+            fakeSurfaceTextureEntry,
+            fakeVideoPlayerOptions,
+            fakeEventSink,
+            httpDataSourceFactorySpy);
+
+    doAnswer(
+            (Answer<Void>)
+                invocation -> {
+                  Map<String, Object> event = new HashMap<>();
+                  event.put("event", "isPlayingStateUpdate");
+                  event.put("isPlaying", (Boolean) invocation.getArguments()[0]);
+                  fakeEventSink.success(event);
+                  return null;
+                })
+        .when(fakeExoPlayer)
+        .setPlayWhenReady(anyBoolean());
+
+    videoPlayer.play();
+
+    verify(fakeEventSink).success(eventCaptor.capture());
+    HashMap<String, Object> event1 = eventCaptor.getValue();
+
+    assertEquals(event1.get("event"), "isPlayingStateUpdate");
+    assertEquals(event1.get("isPlaying"), true);
+
+    videoPlayer.pause();
+
+    verify(fakeEventSink, times(2)).success(eventCaptor.capture());
+    HashMap<String, Object> event2 = eventCaptor.getValue();
+
+    assertEquals(event2.get("event"), "isPlayingStateUpdate");
+    assertEquals(event2.get("isPlaying"), false);
+  }
 }
diff --git a/packages/video_player/video_player_android/example/lib/mini_controller.dart b/packages/video_player/video_player_android/example/lib/mini_controller.dart
index 55f08fe..cea87ba 100644
--- a/packages/video_player/video_player_android/example/lib/mini_controller.dart
+++ b/packages/video_player/video_player_android/example/lib/mini_controller.dart
@@ -8,6 +8,7 @@
 import 'dart:async';
 import 'dart:io';
 
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:video_player_platform_interface/video_player_platform_interface.dart';
@@ -24,10 +25,11 @@
 
 /// The duration, current position, buffering state, error state and settings
 /// of a [MiniController].
+@immutable
 class VideoPlayerValue {
   /// Constructs a video with the given values. Only [duration] is required. The
   /// rest will initialize with default values when unset.
-  VideoPlayerValue({
+  const VideoPlayerValue({
     required this.duration,
     this.size = Size.zero,
     this.position = Duration.zero,
@@ -40,11 +42,11 @@
   });
 
   /// Returns an instance for a video that hasn't been loaded.
-  VideoPlayerValue.uninitialized()
+  const VideoPlayerValue.uninitialized()
       : this(duration: Duration.zero, isInitialized: false);
 
   /// Returns an instance with the given [errorDescription].
-  VideoPlayerValue.erroneous(String errorDescription)
+  const VideoPlayerValue.erroneous(String errorDescription)
       : this(
             duration: Duration.zero,
             isInitialized: false,
@@ -127,6 +129,34 @@
       errorDescription: errorDescription ?? this.errorDescription,
     );
   }
+
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      other is VideoPlayerValue &&
+          runtimeType == other.runtimeType &&
+          duration == other.duration &&
+          position == other.position &&
+          listEquals(buffered, other.buffered) &&
+          isPlaying == other.isPlaying &&
+          isBuffering == other.isBuffering &&
+          playbackSpeed == other.playbackSpeed &&
+          errorDescription == other.errorDescription &&
+          size == other.size &&
+          isInitialized == other.isInitialized;
+
+  @override
+  int get hashCode => Object.hash(
+        duration,
+        position,
+        buffered,
+        isPlaying,
+        isBuffering,
+        playbackSpeed,
+        errorDescription,
+        size,
+        isInitialized,
+      );
 }
 
 /// A very minimal version of `VideoPlayerController` for running the example
@@ -139,21 +169,21 @@
   /// package and null otherwise.
   MiniController.asset(this.dataSource, {this.package})
       : dataSourceType = DataSourceType.asset,
-        super(VideoPlayerValue(duration: Duration.zero));
+        super(const VideoPlayerValue(duration: Duration.zero));
 
   /// Constructs a [MiniController] playing a video from obtained from
   /// the network.
   MiniController.network(this.dataSource)
       : dataSourceType = DataSourceType.network,
         package = null,
-        super(VideoPlayerValue(duration: Duration.zero));
+        super(const VideoPlayerValue(duration: Duration.zero));
 
   /// Constructs a [MiniController] playing a video from obtained from a file.
   MiniController.file(File file)
       : dataSource = Uri.file(file.absolute.path).toString(),
         dataSourceType = DataSourceType.file,
         package = null,
-        super(VideoPlayerValue(duration: Duration.zero));
+        super(const VideoPlayerValue(duration: Duration.zero));
 
   /// The URI to the video file. This will be in different formats depending on
   /// the [DataSourceType] of the original video.
@@ -219,7 +249,6 @@
     final Completer<void> initializingCompleter = Completer<void>();
 
     void eventListener(VideoEvent event) {
-      // ignore: missing_enum_constant_in_switch
       switch (event.eventType) {
         case VideoEventType.initialized:
           value = value.copyWith(
@@ -244,6 +273,9 @@
         case VideoEventType.bufferingEnd:
           value = value.copyWith(isBuffering: false);
           break;
+        case VideoEventType.isPlayingStateUpdate:
+          value = value.copyWith(isPlaying: event.isPlaying);
+          break;
         case VideoEventType.unknown:
           break;
       }
diff --git a/packages/video_player/video_player_android/example/pubspec.yaml b/packages/video_player/video_player_android/example/pubspec.yaml
index 2b4bb09..e11d753 100644
--- a/packages/video_player/video_player_android/example/pubspec.yaml
+++ b/packages/video_player/video_player_android/example/pubspec.yaml
@@ -16,7 +16,7 @@
     # The example app is bundled with the plugin so we use a path dependency on
     # the parent directory to use the current plugin's version.
     path: ../
-  video_player_platform_interface: ">=5.1.1 <7.0.0"
+  video_player_platform_interface: ">=6.1.0 <7.0.0"
 
 dev_dependencies:
   flutter_driver:
diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart
index c8a2b7a..3d34fc8 100644
--- a/packages/video_player/video_player_android/lib/src/android_video_player.dart
+++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart
@@ -148,6 +148,11 @@
           return VideoEvent(eventType: VideoEventType.bufferingStart);
         case 'bufferingEnd':
           return VideoEvent(eventType: VideoEventType.bufferingEnd);
+        case 'isPlayingStateUpdate':
+          return VideoEvent(
+            eventType: VideoEventType.isPlayingStateUpdate,
+            isPlaying: map['isPlaying'] as bool,
+          );
         default:
           return VideoEvent(eventType: VideoEventType.unknown);
       }
diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml
index b847a6d..a77b646 100644
--- a/packages/video_player/video_player_android/pubspec.yaml
+++ b/packages/video_player/video_player_android/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Android implementation of the video_player plugin.
 repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
-version: 2.4.3
+version: 2.4.4
 
 environment:
   sdk: ">=2.18.0 <4.0.0"
@@ -20,7 +20,7 @@
 dependencies:
   flutter:
     sdk: flutter
-  video_player_platform_interface: ">=5.1.1 <7.0.0"
+  video_player_platform_interface: ">=6.1.0 <7.0.0"
 
 dev_dependencies:
   flutter_test:
diff --git a/packages/video_player/video_player_android/test/android_video_player_test.dart b/packages/video_player/video_player_android/test/android_video_player_test.dart
index 00feb27..d30d1d4 100644
--- a/packages/video_player/video_player_android/test/android_video_player_test.dart
+++ b/packages/video_player/video_player_android/test/android_video_player_test.dart
@@ -246,10 +246,11 @@
     });
 
     test('videoEventsFor', () async {
+      const String mockChannel = 'flutter.io/videoPlayer/videoEvents123';
       _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
           .defaultBinaryMessenger
           .setMockMessageHandler(
-        'flutter.io/videoPlayer/videoEvents123',
+        mockChannel,
         (ByteData? message) async {
           final MethodCall methodCall =
               const StandardMethodCodec().decodeMethodCall(message);
@@ -257,7 +258,7 @@
             await _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
                 .defaultBinaryMessenger
                 .handlePlatformMessage(
-                    'flutter.io/videoPlayer/videoEvents123',
+                    mockChannel,
                     const StandardMethodCodec()
                         .encodeSuccessEnvelope(<String, dynamic>{
                       'event': 'initialized',
@@ -270,7 +271,7 @@
             await _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
                 .defaultBinaryMessenger
                 .handlePlatformMessage(
-                    'flutter.io/videoPlayer/videoEvents123',
+                    mockChannel,
                     const StandardMethodCodec()
                         .encodeSuccessEnvelope(<String, dynamic>{
                       'event': 'initialized',
@@ -284,7 +285,7 @@
             await _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
                 .defaultBinaryMessenger
                 .handlePlatformMessage(
-                    'flutter.io/videoPlayer/videoEvents123',
+                    mockChannel,
                     const StandardMethodCodec()
                         .encodeSuccessEnvelope(<String, dynamic>{
                       'event': 'completed',
@@ -294,7 +295,7 @@
             await _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
                 .defaultBinaryMessenger
                 .handlePlatformMessage(
-                    'flutter.io/videoPlayer/videoEvents123',
+                    mockChannel,
                     const StandardMethodCodec()
                         .encodeSuccessEnvelope(<String, dynamic>{
                       'event': 'bufferingUpdate',
@@ -308,7 +309,7 @@
             await _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
                 .defaultBinaryMessenger
                 .handlePlatformMessage(
-                    'flutter.io/videoPlayer/videoEvents123',
+                    mockChannel,
                     const StandardMethodCodec()
                         .encodeSuccessEnvelope(<String, dynamic>{
                       'event': 'bufferingStart',
@@ -318,13 +319,35 @@
             await _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
                 .defaultBinaryMessenger
                 .handlePlatformMessage(
-                    'flutter.io/videoPlayer/videoEvents123',
+                    mockChannel,
                     const StandardMethodCodec()
                         .encodeSuccessEnvelope(<String, dynamic>{
                       'event': 'bufferingEnd',
                     }),
                     (ByteData? data) {});
 
+            await _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
+                .defaultBinaryMessenger
+                .handlePlatformMessage(
+                    mockChannel,
+                    const StandardMethodCodec()
+                        .encodeSuccessEnvelope(<String, dynamic>{
+                      'event': 'isPlayingStateUpdate',
+                      'isPlaying': true,
+                    }),
+                    (ByteData? data) {});
+
+            await _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
+                .defaultBinaryMessenger
+                .handlePlatformMessage(
+                    mockChannel,
+                    const StandardMethodCodec()
+                        .encodeSuccessEnvelope(<String, dynamic>{
+                      'event': 'isPlayingStateUpdate',
+                      'isPlaying': false,
+                    }),
+                    (ByteData? data) {});
+
             return const StandardMethodCodec().encodeSuccessEnvelope(null);
           } else if (methodCall.method == 'cancel') {
             return const StandardMethodCodec().encodeSuccessEnvelope(null);
@@ -363,6 +386,14 @@
                 ]),
             VideoEvent(eventType: VideoEventType.bufferingStart),
             VideoEvent(eventType: VideoEventType.bufferingEnd),
+            VideoEvent(
+              eventType: VideoEventType.isPlayingStateUpdate,
+              isPlaying: true,
+            ),
+            VideoEvent(
+              eventType: VideoEventType.isPlayingStateUpdate,
+              isPlaying: false,
+            ),
           ]));
     });
   });
diff --git a/packages/video_player/video_player_avfoundation/AUTHORS b/packages/video_player/video_player_avfoundation/AUTHORS
index 493a0b4..fc16c35 100644
--- a/packages/video_player/video_player_avfoundation/AUTHORS
+++ b/packages/video_player/video_player_avfoundation/AUTHORS
@@ -64,3 +64,4 @@
 Anton Borries <mail@antonborri.es>
 Alex Li <google@alexv525.com>
 Rahul Raj <64.rahulraj@gmail.com>
+Márton Matuz <matuzmarci@gmail.com>
diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md
index b2308dd..ef73b92 100644
--- a/packages/video_player/video_player_avfoundation/CHANGELOG.md
+++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.4.3
+
+* Synchronizes `VideoPlayerValue.isPlaying` with `AVPlayer`.
+
 ## 2.4.2
 
 * Makes seekTo async and only complete when AVPlayer.seekTo completes.
diff --git a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart
index 55f08fe..cea87ba 100644
--- a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart
+++ b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart
@@ -8,6 +8,7 @@
 import 'dart:async';
 import 'dart:io';
 
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:video_player_platform_interface/video_player_platform_interface.dart';
@@ -24,10 +25,11 @@
 
 /// The duration, current position, buffering state, error state and settings
 /// of a [MiniController].
+@immutable
 class VideoPlayerValue {
   /// Constructs a video with the given values. Only [duration] is required. The
   /// rest will initialize with default values when unset.
-  VideoPlayerValue({
+  const VideoPlayerValue({
     required this.duration,
     this.size = Size.zero,
     this.position = Duration.zero,
@@ -40,11 +42,11 @@
   });
 
   /// Returns an instance for a video that hasn't been loaded.
-  VideoPlayerValue.uninitialized()
+  const VideoPlayerValue.uninitialized()
       : this(duration: Duration.zero, isInitialized: false);
 
   /// Returns an instance with the given [errorDescription].
-  VideoPlayerValue.erroneous(String errorDescription)
+  const VideoPlayerValue.erroneous(String errorDescription)
       : this(
             duration: Duration.zero,
             isInitialized: false,
@@ -127,6 +129,34 @@
       errorDescription: errorDescription ?? this.errorDescription,
     );
   }
+
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      other is VideoPlayerValue &&
+          runtimeType == other.runtimeType &&
+          duration == other.duration &&
+          position == other.position &&
+          listEquals(buffered, other.buffered) &&
+          isPlaying == other.isPlaying &&
+          isBuffering == other.isBuffering &&
+          playbackSpeed == other.playbackSpeed &&
+          errorDescription == other.errorDescription &&
+          size == other.size &&
+          isInitialized == other.isInitialized;
+
+  @override
+  int get hashCode => Object.hash(
+        duration,
+        position,
+        buffered,
+        isPlaying,
+        isBuffering,
+        playbackSpeed,
+        errorDescription,
+        size,
+        isInitialized,
+      );
 }
 
 /// A very minimal version of `VideoPlayerController` for running the example
@@ -139,21 +169,21 @@
   /// package and null otherwise.
   MiniController.asset(this.dataSource, {this.package})
       : dataSourceType = DataSourceType.asset,
-        super(VideoPlayerValue(duration: Duration.zero));
+        super(const VideoPlayerValue(duration: Duration.zero));
 
   /// Constructs a [MiniController] playing a video from obtained from
   /// the network.
   MiniController.network(this.dataSource)
       : dataSourceType = DataSourceType.network,
         package = null,
-        super(VideoPlayerValue(duration: Duration.zero));
+        super(const VideoPlayerValue(duration: Duration.zero));
 
   /// Constructs a [MiniController] playing a video from obtained from a file.
   MiniController.file(File file)
       : dataSource = Uri.file(file.absolute.path).toString(),
         dataSourceType = DataSourceType.file,
         package = null,
-        super(VideoPlayerValue(duration: Duration.zero));
+        super(const VideoPlayerValue(duration: Duration.zero));
 
   /// The URI to the video file. This will be in different formats depending on
   /// the [DataSourceType] of the original video.
@@ -219,7 +249,6 @@
     final Completer<void> initializingCompleter = Completer<void>();
 
     void eventListener(VideoEvent event) {
-      // ignore: missing_enum_constant_in_switch
       switch (event.eventType) {
         case VideoEventType.initialized:
           value = value.copyWith(
@@ -244,6 +273,9 @@
         case VideoEventType.bufferingEnd:
           value = value.copyWith(isBuffering: false);
           break;
+        case VideoEventType.isPlayingStateUpdate:
+          value = value.copyWith(isPlaying: event.isPlaying);
+          break;
         case VideoEventType.unknown:
           break;
       }
diff --git a/packages/video_player/video_player_avfoundation/example/pubspec.yaml b/packages/video_player/video_player_avfoundation/example/pubspec.yaml
index f8b9ca6..bffa17d 100644
--- a/packages/video_player/video_player_avfoundation/example/pubspec.yaml
+++ b/packages/video_player/video_player_avfoundation/example/pubspec.yaml
@@ -16,7 +16,7 @@
     # The example app is bundled with the plugin so we use a path dependency on
     # the parent directory to use the current plugin's version.
     path: ../
-  video_player_platform_interface: ">=4.2.0 <7.0.0"
+  video_player_platform_interface: ">=6.1.0 <7.0.0"
 
 dev_dependencies:
   flutter_driver:
diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m
index ce0a121..586d655 100644
--- a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m
+++ b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m
@@ -62,6 +62,7 @@
 static void *playbackLikelyToKeepUpContext = &playbackLikelyToKeepUpContext;
 static void *playbackBufferEmptyContext = &playbackBufferEmptyContext;
 static void *playbackBufferFullContext = &playbackBufferFullContext;
+static void *rateContext = &rateContext;
 
 @implementation FLTVideoPlayer
 - (instancetype)initWithAsset:(NSString *)asset frameUpdater:(FLTFrameUpdater *)frameUpdater {
@@ -69,7 +70,7 @@
   return [self initWithURL:[NSURL fileURLWithPath:path] frameUpdater:frameUpdater httpHeaders:@{}];
 }
 
-- (void)addObservers:(AVPlayerItem *)item {
+- (void)addObserversForItem:(AVPlayerItem *)item player:(AVPlayer *)player {
   [item addObserver:self
          forKeyPath:@"loadedTimeRanges"
             options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
@@ -99,6 +100,13 @@
             options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
             context:playbackBufferFullContext];
 
+  // Add observer to AVPlayer instead of AVPlayerItem since the AVPlayerItem does not have a "rate"
+  // property
+  [player addObserver:self
+           forKeyPath:@"rate"
+              options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
+              context:rateContext];
+
   // Add an observer that will respond to itemDidPlayToEndTime
   [[NSNotificationCenter defaultCenter] addObserver:self
                                            selector:@selector(itemDidPlayToEndTime:)
@@ -252,7 +260,7 @@
 
   [self createVideoOutputAndDisplayLink:frameUpdater];
 
-  [self addObservers:item];
+  [self addObserversForItem:item player:_player];
 
   [asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ] completionHandler:assetCompletionHandler];
 
@@ -317,6 +325,14 @@
     if (_eventSink != nil) {
       _eventSink(@{@"event" : @"bufferingEnd"});
     }
+  } else if (context == rateContext) {
+    // Important: Make sure to cast the object to AVPlayer when observing the rate property,
+    // as it is not available in AVPlayerItem.
+    AVPlayer *player = (AVPlayer *)object;
+    if (_eventSink != nil) {
+      _eventSink(
+          @{@"event" : @"isPlayingStateUpdate", @"isPlaying" : player.rate > 0 ? @YES : @NO});
+    }
   }
 }
 
diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart
index b5ebedd..868c598 100644
--- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart
+++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart
@@ -146,6 +146,11 @@
           return VideoEvent(eventType: VideoEventType.bufferingStart);
         case 'bufferingEnd':
           return VideoEvent(eventType: VideoEventType.bufferingEnd);
+        case 'isPlayingStateUpdate':
+          return VideoEvent(
+            eventType: VideoEventType.isPlayingStateUpdate,
+            isPlaying: map['isPlaying'] as bool,
+          );
         default:
           return VideoEvent(eventType: VideoEventType.unknown);
       }
diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml
index 9f5566c..6cb0565 100644
--- a/packages/video_player/video_player_avfoundation/pubspec.yaml
+++ b/packages/video_player/video_player_avfoundation/pubspec.yaml
@@ -2,7 +2,7 @@
 description: iOS implementation of the video_player plugin.
 repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
-version: 2.4.2
+version: 2.4.3
 
 environment:
   sdk: ">=2.18.0 <4.0.0"
@@ -19,7 +19,7 @@
 dependencies:
   flutter:
     sdk: flutter
-  video_player_platform_interface: ">=4.2.0 <7.0.0"
+  video_player_platform_interface: ">=6.1.0 <7.0.0"
 
 dev_dependencies:
   flutter_test:
diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart
index a0f37e2..1b25da8 100644
--- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart
+++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart
@@ -234,10 +234,11 @@
     });
 
     test('videoEventsFor', () async {
+      const String mockChannel = 'flutter.io/videoPlayer/videoEvents123';
       _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
           .defaultBinaryMessenger
           .setMockMessageHandler(
-        'flutter.io/videoPlayer/videoEvents123',
+        mockChannel,
         (ByteData? message) async {
           final MethodCall methodCall =
               const StandardMethodCodec().decodeMethodCall(message);
@@ -245,7 +246,7 @@
             await _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
                 .defaultBinaryMessenger
                 .handlePlatformMessage(
-                    'flutter.io/videoPlayer/videoEvents123',
+                    mockChannel,
                     const StandardMethodCodec()
                         .encodeSuccessEnvelope(<String, dynamic>{
                       'event': 'initialized',
@@ -258,7 +259,7 @@
             await _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
                 .defaultBinaryMessenger
                 .handlePlatformMessage(
-                    'flutter.io/videoPlayer/videoEvents123',
+                    mockChannel,
                     const StandardMethodCodec()
                         .encodeSuccessEnvelope(<String, dynamic>{
                       'event': 'completed',
@@ -268,7 +269,7 @@
             await _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
                 .defaultBinaryMessenger
                 .handlePlatformMessage(
-                    'flutter.io/videoPlayer/videoEvents123',
+                    mockChannel,
                     const StandardMethodCodec()
                         .encodeSuccessEnvelope(<String, dynamic>{
                       'event': 'bufferingUpdate',
@@ -282,7 +283,7 @@
             await _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
                 .defaultBinaryMessenger
                 .handlePlatformMessage(
-                    'flutter.io/videoPlayer/videoEvents123',
+                    mockChannel,
                     const StandardMethodCodec()
                         .encodeSuccessEnvelope(<String, dynamic>{
                       'event': 'bufferingStart',
@@ -292,13 +293,35 @@
             await _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
                 .defaultBinaryMessenger
                 .handlePlatformMessage(
-                    'flutter.io/videoPlayer/videoEvents123',
+                    mockChannel,
                     const StandardMethodCodec()
                         .encodeSuccessEnvelope(<String, dynamic>{
                       'event': 'bufferingEnd',
                     }),
                     (ByteData? data) {});
 
+            await _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
+                .defaultBinaryMessenger
+                .handlePlatformMessage(
+                    mockChannel,
+                    const StandardMethodCodec()
+                        .encodeSuccessEnvelope(<String, dynamic>{
+                      'event': 'isPlayingStateUpdate',
+                      'isPlaying': true,
+                    }),
+                    (ByteData? data) {});
+
+            await _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
+                .defaultBinaryMessenger
+                .handlePlatformMessage(
+                    mockChannel,
+                    const StandardMethodCodec()
+                        .encodeSuccessEnvelope(<String, dynamic>{
+                      'event': 'isPlayingStateUpdate',
+                      'isPlaying': false,
+                    }),
+                    (ByteData? data) {});
+
             return const StandardMethodCodec().encodeSuccessEnvelope(null);
           } else if (methodCall.method == 'cancel') {
             return const StandardMethodCodec().encodeSuccessEnvelope(null);
@@ -330,6 +353,14 @@
                 ]),
             VideoEvent(eventType: VideoEventType.bufferingStart),
             VideoEvent(eventType: VideoEventType.bufferingEnd),
+            VideoEvent(
+              eventType: VideoEventType.isPlayingStateUpdate,
+              isPlaying: true,
+            ),
+            VideoEvent(
+              eventType: VideoEventType.isPlayingStateUpdate,
+              isPlaying: false,
+            ),
           ]));
     });
   });
diff --git a/packages/video_player/video_player_web/AUTHORS b/packages/video_player/video_player_web/AUTHORS
index 493a0b4..fc16c35 100644
--- a/packages/video_player/video_player_web/AUTHORS
+++ b/packages/video_player/video_player_web/AUTHORS
@@ -64,3 +64,4 @@
 Anton Borries <mail@antonborri.es>
 Alex Li <google@alexv525.com>
 Rahul Raj <64.rahulraj@gmail.com>
+Márton Matuz <matuzmarci@gmail.com>
diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md
index 160c154..19f5bb9 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.16
+
+* Synchronizes `VideoPlayerValue.isPlaying` with `VideoElement`.
+
 ## 2.0.15
 
 * Clarifies explanation of endorsement in README.
diff --git a/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart b/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart
index 5053ea6..7d74223 100644
--- a/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart
+++ b/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart
@@ -169,13 +169,47 @@
       expect(VideoPlayerPlatform.instance.setMixWithOthers(false), completes);
     });
 
+    testWidgets(
+        'double call to play will emit a single isPlayingStateUpdate event',
+        (WidgetTester tester) async {
+      final int videoPlayerId = await textureId;
+      final Stream<VideoEvent> eventStream =
+          VideoPlayerPlatform.instance.videoEventsFor(videoPlayerId);
+
+      final Future<List<VideoEvent>> stream = eventStream.timeout(
+        const Duration(seconds: 2),
+        onTimeout: (EventSink<VideoEvent> sink) {
+          sink.close();
+        },
+      ).toList();
+
+      await VideoPlayerPlatform.instance.setVolume(videoPlayerId, 0);
+      await VideoPlayerPlatform.instance.play(videoPlayerId);
+      await VideoPlayerPlatform.instance.play(videoPlayerId);
+
+      // Let the video play, until we stop seeing events for two seconds
+      final List<VideoEvent> events = await stream;
+
+      await VideoPlayerPlatform.instance.pause(videoPlayerId);
+
+      expect(
+          events.where((VideoEvent e) =>
+              e.eventType == VideoEventType.isPlayingStateUpdate),
+          equals(<VideoEvent>[
+            VideoEvent(
+              eventType: VideoEventType.isPlayingStateUpdate,
+              isPlaying: true,
+            )
+          ]));
+    });
+
     testWidgets('video playback lifecycle', (WidgetTester tester) async {
       final int videoPlayerId = await textureId;
       final Stream<VideoEvent> eventStream =
           VideoPlayerPlatform.instance.videoEventsFor(videoPlayerId);
 
       final Future<List<VideoEvent>> stream = eventStream.timeout(
-        const Duration(seconds: 1),
+        const Duration(seconds: 2),
         onTimeout: (EventSink<VideoEvent> sink) {
           sink.close();
         },
@@ -184,23 +218,25 @@
       await VideoPlayerPlatform.instance.setVolume(videoPlayerId, 0);
       await VideoPlayerPlatform.instance.play(videoPlayerId);
 
-      // Let the video play, until we stop seeing events for a second
+      // Let the video play, until we stop seeing events for two seconds
       final List<VideoEvent> events = await stream;
 
       await VideoPlayerPlatform.instance.pause(videoPlayerId);
 
       // The expected list of event types should look like this:
-      // 1. bufferingStart,
-      // 2. bufferingUpdate (videoElement.onWaiting),
-      // 3. initialized (videoElement.onCanPlay),
-      // 4. bufferingEnd (videoElement.onCanPlayThrough),
+      // 1. isPlayingStateUpdate (videoElement.onPlaying)
+      // 2. bufferingStart,
+      // 3. bufferingUpdate (videoElement.onWaiting),
+      // 4. initialized (videoElement.onCanPlay),
+      // 5. bufferingEnd (videoElement.onCanPlayThrough),
       expect(
           events.map((VideoEvent e) => e.eventType),
           equals(<VideoEventType>[
+            VideoEventType.isPlayingStateUpdate,
             VideoEventType.bufferingStart,
             VideoEventType.bufferingUpdate,
             VideoEventType.initialized,
-            VideoEventType.bufferingEnd
+            VideoEventType.bufferingEnd,
           ]));
     });
   });
diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml
index 3114335..4cb1d4d 100644
--- a/packages/video_player/video_player_web/example/pubspec.yaml
+++ b/packages/video_player/video_player_web/example/pubspec.yaml
@@ -9,7 +9,7 @@
   flutter:
     sdk: flutter
   js: ^0.6.0
-  video_player_platform_interface: ">=4.2.0 <7.0.0"
+  video_player_platform_interface: ">=6.1.0 <7.0.0"
   video_player_web:
     path: ../
 
diff --git a/packages/video_player/video_player_web/lib/src/video_player.dart b/packages/video_player/video_player_web/lib/src/video_player.dart
index 02ead1f..bc0021d 100644
--- a/packages/video_player/video_player_web/lib/src/video_player.dart
+++ b/packages/video_player/video_player_web/lib/src/video_player.dart
@@ -102,6 +102,20 @@
       ));
     });
 
+    _videoElement.onPlay.listen((dynamic _) {
+      _eventController.add(VideoEvent(
+        eventType: VideoEventType.isPlayingStateUpdate,
+        isPlaying: true,
+      ));
+    });
+
+    _videoElement.onPause.listen((dynamic _) {
+      _eventController.add(VideoEvent(
+        eventType: VideoEventType.isPlayingStateUpdate,
+        isPlaying: false,
+      ));
+    });
+
     _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 07d9b84..70434ef 100644
--- a/packages/video_player/video_player_web/pubspec.yaml
+++ b/packages/video_player/video_player_web/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Web platform implementation of video_player.
 repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_web
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
-version: 2.0.15
+version: 2.0.16
 
 environment:
   sdk: ">=2.17.0 <4.0.0"
@@ -21,7 +21,7 @@
     sdk: flutter
   flutter_web_plugins:
     sdk: flutter
-  video_player_platform_interface: ">=4.2.0 <7.0.0"
+  video_player_platform_interface: ">=6.1.0 <7.0.0"
 
 dev_dependencies:
   flutter_test: