Repository: fluttercommunity/chewie
Pull request #912: Add workaround for invalid buffering info on android
Author: login: timoxd7
Created: 2025-04-02T10:10:28Z | Merged: 2025-04-09T20:26:14Z

## Info
As already described in multiple issues, there is currently a problem with android reporting "isBuffering" with true even if the video is already playing, preventing the video controls to be displayed. This is a workaround, checking (on android only) if the video is actually buffering. Tested with a network- and file-based video.

>NOTE: This is only a workaround until video_player and flutter is fixed. See https://github.com/flutter/flutter/issues/165149 for more information. Also thanks to @wenwangwill who added a workaround as comment i got inspired from.

## Additional Issues not solved
If pausing the video and seeking to some point (using seekTo, so clicking on the progress bar) still leads to the infinit loading bar, as no callback is called by the video controller and thus the buffering not updated.

As workaround, just start playing on seekTo like this:

```dart
  // Your init code can be above
  videoController.addListener(yourListeningMethod);

  // ...

  bool wasPlayingBefore = false;
  void yourListeningMethod() {
    if (!videoController.value.isPlaying && !wasPlayingBefore) {
      // -> Workaround if seekTo another position while it was paused before
      //    on Android this might lead to infinite loading, so just play the
      //    video again.
      videoController.play();
    }

    wasPlayingBefore = videoController.value.isPlaying;

  // ...
  }

```

## Related to
#909
#907 
#906
#903



Diff:
diff --git a/README.md b/README.md
index 72884a3be..a6bcb1e4b 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,8 @@ Chewie uses the `video_player` under the hood and wraps it in a friendly Materia
 8.  🧪 [Example](#-example)
 9.  ⏪ [Migrating from Chewie < 0.9.0](#-migrating-from-chewie--090)
 10. 🗺️ [Roadmap](#%EF%B8%8F-roadmap)
-11. 📱 [iOS warning](#-ios-warning-)
+11. ⚠️ [Android warning](#%EF%B8%8F-android-warning)
+12. 📱 [iOS warning](#-ios-warning)
 
 
 ## 🚨 IMPORTANT!!! (READ THIS FIRST)
@@ -286,6 +287,42 @@ final playerWidget = Chewie(
 - [ ] Screen-Mirroring / Casting (Google Chromecast)
 
 
+## ⚠️ Android warning
+
+There is an open [issue](https://github.com/flutter/flutter/issues/165149) that the buffering state of a video is not reported correctly. With this, the loading state is always triggered, hiding controls to play, pause or seek the video. A workaround was implemented until this is fixed, however it can't be perfect and still hides controls if seeking backwards while the video is paused, as a result of lack of correct buffering information (see #912).
+
+Add the following to partly fix this behavior:
+
+```dart
+  // Your init code can be above
+  videoController.addListener(yourListeningMethod);
+
+  // ...
+
+  bool wasPlayingBefore = false;
+  void yourListeningMethod() {
+    if (!videoController.value.isPlaying && !wasPlayingBefore) {
+      // -> Workaround if seekTo another position while it was paused before.
+      //    On Android this might lead to infinite loading, so just play the
+      //    video again.
+      videoController.play();
+    }
+
+    wasPlayingBefore = videoController.value.isPlaying;
+
+  // ...
+  }
+```
+
+You can also disable the loading spinner entirely to fix this problem in a more _complete_ way, however will remove the loading indicator if a video is buffering.
+
+```dart
+_chewieController = ChewieController(
+  videoPlayerController: _videoPlayerController,
+  progressIndicatorDelay: Platform.isAndroid ? const Duration(days: 1) : null,
+);
+```
+
 ## 📱 iOS warning 
 
 The video_player plugin used by chewie will only work in iOS simulators if you are on flutter 1.26.0 or above. You may need to switch to the beta channel `flutter channel beta`
diff --git a/lib/src/cupertino/cupertino_controls.dart b/lib/src/cupertino/cupertino_controls.dart
index db7267836..b0488a275 100644
--- a/lib/src/cupertino/cupertino_controls.dart
+++ b/lib/src/cupertino/cupertino_controls.dart
@@ -810,9 +810,11 @@ class _CupertinoControlsState extends State<CupertinoControls>
   void _updateState() {
     if (!mounted) return;
 
+    final bool buffering = getIsBuffering(controller);
+
     // display the progress bar indicator only after the buffering delay if it has been set
     if (chewieController.progressIndicatorDelay != null) {
-      if (controller.value.isBuffering) {
+      if (buffering) {
         _bufferingDisplayTimer ??= Timer(
           chewieController.progressIndicatorDelay!,
           _bufferingTimerTimeout,
@@ -823,7 +825,7 @@ class _CupertinoControlsState extends State<CupertinoControls>
         _displayBufferingIndicator = false;
       }
     } else {
-      _displayBufferingIndicator = controller.value.isBuffering;
+      _displayBufferingIndicator = buffering;
     }
 
     setState(() {
diff --git a/lib/src/helpers/utils.dart b/lib/src/helpers/utils.dart
index 0f3a2e128..17ae2161a 100644
--- a/lib/src/helpers/utils.dart
+++ b/lib/src/helpers/utils.dart
@@ -1,3 +1,6 @@
+import 'package:flutter/foundation.dart';
+import 'package:video_player/video_player.dart';
+
 String formatDuration(Duration position) {
   final ms = position.inMilliseconds;
 
@@ -30,3 +33,39 @@ String formatDuration(Duration position) {
 
   return formattedTime;
 }
+
+/// Gets the current buffering state of the video player.
+///
+/// For Android, it will use a workaround due to a [bug](https://github.com/flutter/flutter/issues/165149)
+/// affecting the `video_player` plugin, preventing it from getting the
+/// actual buffering state. This currently results in the `VideoPlayerController` always buffering,
+/// thus breaking UI elements.
+///
+/// For this, the actual buffer position is used to determine if the video is
+/// buffering or not. See Issue [#912](https://github.com/fluttercommunity/chewie/pull/912) for more details.
+bool getIsBuffering(VideoPlayerController controller) {
+  final VideoPlayerValue value = controller.value;
+
+  if (defaultTargetPlatform == TargetPlatform.android) {
+    if (value.isBuffering) {
+      // -> Check if we actually buffer, as android has a bug preventing to
+      //    get the correct buffering state from this single bool.
+      final int position = value.position.inMilliseconds;
+
+      // Special case, if the video is finished, we don't want to show the
+      // buffering indicator anymore
+      if (position >= value.duration.inMilliseconds) {
+        return false;
+      } else {
+        final int buffer = value.buffered.lastOrNull?.end.inMilliseconds ?? -1;
+
+        return position >= buffer;
+      }
+    } else {
+      // -> No buffering
+      return false;
+    }
+  }
+
+  return value.isBuffering;
+}
diff --git a/lib/src/material/material_controls.dart b/lib/src/material/material_controls.dart
index 2d813e05a..3ea67f31d 100644
--- a/lib/src/material/material_controls.dart
+++ b/lib/src/material/material_controls.dart
@@ -645,9 +645,11 @@ class _MaterialControlsState extends State<MaterialControls>
   void _updateState() {
     if (!mounted) return;
 
+    final bool buffering = getIsBuffering(controller);
+
     // display the progress bar indicator only after the buffering delay if it has been set
     if (chewieController.progressIndicatorDelay != null) {
-      if (controller.value.isBuffering) {
+      if (buffering) {
         _bufferingDisplayTimer ??= Timer(
           chewieController.progressIndicatorDelay!,
           _bufferingTimerTimeout,
@@ -658,7 +660,7 @@ class _MaterialControlsState extends State<MaterialControls>
         _displayBufferingIndicator = false;
       }
     } else {
-      _displayBufferingIndicator = controller.value.isBuffering;
+      _displayBufferingIndicator = buffering;
     }
 
     setState(() {
diff --git a/lib/src/material/material_desktop_controls.dart b/lib/src/material/material_desktop_controls.dart
index 4b0ba0fe8..ebffd9560 100644
--- a/lib/src/material/material_desktop_controls.dart
+++ b/lib/src/material/material_desktop_controls.dart
@@ -581,9 +581,11 @@ class _MaterialDesktopControlsState extends State<MaterialDesktopControls>
   void _updateState() {
     if (!mounted) return;
 
+    final bool buffering = getIsBuffering(controller);
+
     // display the progress bar indicator only after the buffering delay if it has been set
     if (chewieController.progressIndicatorDelay != null) {
-      if (controller.value.isBuffering) {
+      if (buffering) {
         _bufferingDisplayTimer ??= Timer(
           chewieController.progressIndicatorDelay!,
           _bufferingTimerTimeout,
@@ -594,7 +596,7 @@ class _MaterialDesktopControlsState extends State<MaterialDesktopControls>
         _displayBufferingIndicator = false;
       }
     } else {
-      _displayBufferingIndicator = controller.value.isBuffering;
+      _displayBufferingIndicator = buffering;
     }
 
     setState(() {


Reviews:
user: login: diegotori
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T15:06:37Z
user: login: diegotori
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T13:11:52Z
user: login: diegotori
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T13:14:04Z
user: login: timoxd7
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T13:14:34Z
user: login: timoxd7
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T13:22:26Z
user: login: diegotori
body: For the new utility method, add comments related to the issue your changes are trying to address. 

Thanks.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T13:26:16Z
user: login: diegotori
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T16:06:53Z
user: login: diegotori
body: LGTM.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-09T20:26:07Z

Review comments:
diff_hunk: @@ -810,9 +811,37 @@ class _CupertinoControlsState extends State<CupertinoControls>
   void _updateState() {
     if (!mounted) return;
 
+    late final bool buffering;
+
+    if (Platform.isAndroid) {
+      if (controller.value.isBuffering) {
+        // -> Check if we actually buffer, as android has a bug preventing to
+        //    get the correct buffering state from this single bool.
+        final VideoPlayerValue value = controller.value;
+
+        final int position = value.position.inMilliseconds;
+
+        // Special case, if the video is finished, we don't want to show the
+        // buffering indicator anymore
+        if (position >= value.duration.inMilliseconds) {
+          buffering = false;
+        } else {
+          final int buffer =
+              value.buffered.lastOrNull?.end.inMilliseconds ?? -1;
+
+          buffering = position >= buffer;
+        }
+      } else {
+        // -> No buffering
+        buffering = false;
+      }
+    } else {
+      buffering = controller.value.isBuffering;
+    }
path: lib/src/cupertino/cupertino_controls.dart
user: login: diegotori
body: Encapsulate this into a private function.
created_at: 2025-04-02T15:05:58Z
author_association: COLLABORATOR
diff_hunk: @@ -645,9 +646,37 @@ class _MaterialControlsState extends State<MaterialControls>
   void _updateState() {
     if (!mounted) return;
 
+    late final bool buffering;
+
+    if (Platform.isAndroid) {
+      if (controller.value.isBuffering) {
+        // -> Check if we actually buffer, as android has a bug preventing to
+        //    get the correct buffering state from this single bool.
+        final VideoPlayerValue value = controller.value;
+
+        final int position = value.position.inMilliseconds;
+
+        // Special case, if the video is finished, we don't want to show the
+        // buffering indicator anymore
+        if (position >= value.duration.inMilliseconds) {
+          buffering = false;
+        } else {
+          final int buffer =
+              value.buffered.lastOrNull?.end.inMilliseconds ?? -1;
+
+          buffering = position >= buffer;
+        }
+      } else {
+        // -> No buffering
+        buffering = false;
+      }
+    } else {
+      buffering = controller.value.isBuffering;
+    }
path: lib/src/material/material_controls.dart
user: login: diegotori
body: Encapsulate this into a private function.
created_at: 2025-04-02T15:06:13Z
author_association: COLLABORATOR
diff_hunk: @@ -581,9 +582,37 @@ class _MaterialDesktopControlsState extends State<MaterialDesktopControls>
   void _updateState() {
     if (!mounted) return;
 
+    late final bool buffering;
+
+    if (Platform.isAndroid) {
+      if (controller.value.isBuffering) {
+        // -> Check if we actually buffer, as android has a bug preventing to
+        //    get the correct buffering state from this single bool.
+        final VideoPlayerValue value = controller.value;
+
+        final int position = value.position.inMilliseconds;
+
+        // Special case, if the video is finished, we don't want to show the
+        // buffering indicator anymore
+        if (position >= value.duration.inMilliseconds) {
+          buffering = false;
+        } else {
+          final int buffer =
+              value.buffered.lastOrNull?.end.inMilliseconds ?? -1;
+
+          buffering = position >= buffer;
+        }
+      } else {
+        // -> No buffering
+        buffering = false;
+      }
+    } else {
+      buffering = controller.value.isBuffering;
+    }
path: lib/src/material/material_desktop_controls.dart
user: login: diegotori
body: Encapsulate this into a private function.
created_at: 2025-04-02T15:06:29Z
author_association: COLLABORATOR
diff_hunk: @@ -1,3 +1,7 @@
+import 'dart:io';
path: lib/src/helpers/utils.dart
user: login: diegotori
body: Please ensure that this doesn't use 'dart:io' since it's not compatible with web. I believe you have to use `TargetPlatform`instead of `Platform`.
created_at: 2025-04-07T13:11:49Z
author_association: COLLABORATOR
diff_hunk: @@ -1,3 +1,7 @@
+import 'dart:io';
path: lib/src/helpers/utils.dart
user: login: diegotori
body: Instead, have that method take in a `BuildContext` and then use `(Theme.of(context).platform` to find the current platform.
created_at: 2025-04-07T13:14:04Z
author_association: COLLABORATOR
diff_hunk: @@ -1,3 +1,7 @@
+import 'dart:io';
path: lib/src/helpers/utils.dart
user: login: timoxd7
body: Will fix it!
created_at: 2025-04-07T13:14:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,3 +1,7 @@
+import 'dart:io';
path: lib/src/helpers/utils.dart
user: login: timoxd7
body: Just saw your second comment. Should i change it still to `BuildContext`?
created_at: 2025-04-07T13:22:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,6 +34,11 @@ String formatDuration(Duration position) {
   return formattedTime;
 }
 
+/// Get the current buffering state of the video player. Will invoke a
+/// Workaround for Android, as a bug in the video_player plugin prevents to get
+/// the actual buffering state, always returning true and breaking ui elements.
+/// For this, the actual buffer position is used to determine if the video is
+/// buffering or not. See #912 for more details.
path: lib/src/helpers/utils.dart
user: login: diegotori
body: Change this to:

```dart
/// Gets the current buffering state of the video player. 
/// 
/// For Android, it will use a workaround due to a [bug](https://github.com/flutter/flutter/issues/165149) 
/// affecting the `video_player` plugin, preventing it from getting the 
/// actual buffering state. This currently results in the `VideoPlayerController` always buffering,
/// thus breaking UI elements.
///
/// For this, the actual buffer position is used to determine if the video is
/// buffering or not. See Issue [#912](https://github.com/fluttercommunity/chewie/pull/912) for more details.
```
created_at: 2025-04-07T16:05:38Z
author_association: COLLABORATOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-02T12:51:39Z
body: ## [Codecov](https://app.codecov.io/gh/fluttercommunity/chewie/pull/912?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fluttercommunity) Report
Attention: Patch coverage is `66.66667%` with `5 lines` in your changes missing coverage. Please review.
> Project coverage is 44.74%. Comparing base [(`b0ccd83`)](https://app.codecov.io/gh/fluttercommunity/chewie/commit/b0ccd83d6c9308523dcd4fe3023dbfa7a9c71f7d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fluttercommunity) to head [(`85cddc5`)](https://app.codecov.io/gh/fluttercommunity/chewie/commit/85cddc568736e25b29b76516d3b708eef4210420?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fluttercommunity).
> Report is 14 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/fluttercommunity/chewie/pull/912?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fluttercommunity) | Patch % | Lines |
|---|---|---|
| [lib/src/helpers/utils.dart](https://app.codecov.io/gh/fluttercommunity/chewie/pull/912?src=pr&el=tree&filepath=lib%2Fsrc%2Fhelpers%2Futils.dart&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fluttercommunity#diff-bGliL3NyYy9oZWxwZXJzL3V0aWxzLmRhcnQ=) | 44.44% | [5 Missing :warning: ](https://app.codecov.io/gh/fluttercommunity/chewie/pull/912?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fluttercommunity) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #912      +/-   ##
==========================================
- Coverage   44.95%   44.74%   -0.22%     
==========================================
  Files          22       22              
  Lines        1546     1569      +23     
==========================================
+ Hits          695      702       +7     
- Misses        851      867      +16     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/fluttercommunity/chewie/pull/912?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fluttercommunity).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fluttercommunity).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: diegotori
created_at: 2025-04-02T15:07:22Z
body: @timoxd7 can you add that additional workaround in the README so that it doesn't get lost after this version is eventually released? Thanks in advance.
author_association: COLLABORATOR
user: login: yann-elpyoo
created_at: 2025-04-02T20:07:45Z
body: For information until the bug is fixed, **and for chewie users only** : it is possible to use a simple and complete workaround.

The thing is to add a long duration for the loader to appear. Then nothing is blocked anymore even at the end of the video! No need for looping too. It just shows the video paused and played each time seek to is called... 
```
 _chewieController = ChewieController(
          videoPlayerController: _videoPlayerController,
          progressIndicatorDelay:
              PlatformProvider.isAndroid() ? const Duration(days: 1) : null,
        );
```

For non chewie users suffering from it, maybe wrapping with chewie could be a temporary solution...
author_association: NONE
user: login: naghinezhad
created_at: 2025-04-07T07:51:20Z
body: @timoxd7 @diegotori
Please review and merge this pull request so the related bug can be resolved. We are waiting for it.
author_association: NONE
user: login: timoxd7
created_at: 2025-04-07T13:12:34Z
body: @naghinezhad i am a bit in two minds about this pr. I will incorporate the requested changes, but it still is not a complete fix. 

I don't know if it works good with live streams. As @yann-elpyoo wrote, you _could_ fix it by disabling indirectly the loading spinner for android, but this also removes the feedback to the user that something is buffering.

@diegotori i updated according to your review. I also tested @yann-elpyoo solution and added it to the readme, both work together too (as my workaround will just be _hidden_ by disabling the loader anyway).

As maintainer you can decide wether to merge this, as the root problem is not relying in chewie itself. However, we now have it documented here at least and if someone needs a fix, can use it util it is fixed in flutter itself.
author_association: CONTRIBUTOR
user: login: diegotori
created_at: 2025-04-07T16:07:16Z
body: @timoxd7 other than the final nit that I called out, the rest LGTM.
author_association: COLLABORATOR