From a05a72dd294ce66fdaa1154f6ec36517150aa7ce Mon Sep 17 00:00:00 2001 From: timoxd7 Date: Wed, 2 Apr 2025 11:44:28 +0200 Subject: [PATCH 1/9] Add workaround for invalid buffering info on android --- lib/src/cupertino/cupertino_controls.dart | 24 +++++++++++++++++-- lib/src/material/material_controls.dart | 24 +++++++++++++++++-- .../material/material_desktop_controls.dart | 24 +++++++++++++++++-- 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/lib/src/cupertino/cupertino_controls.dart b/lib/src/cupertino/cupertino_controls.dart index db7267836..4e12ee0c4 100644 --- a/lib/src/cupertino/cupertino_controls.dart +++ b/lib/src/cupertino/cupertino_controls.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'dart:math' as math; import 'dart:ui' as ui; @@ -810,9 +811,28 @@ class _CupertinoControlsState extends State 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 buffer = value.buffered.lastOrNull?.end.inMilliseconds ?? -1; + final int position = value.position.inMilliseconds; + + buffering = position >= buffer; + } else { + // -> No buffering + buffering = false; + } + } else { + buffering = controller.value.isBuffering; + } + // 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 +843,7 @@ class _CupertinoControlsState extends State _displayBufferingIndicator = false; } } else { - _displayBufferingIndicator = controller.value.isBuffering; + _displayBufferingIndicator = buffering; } setState(() { diff --git a/lib/src/material/material_controls.dart b/lib/src/material/material_controls.dart index 2d813e05a..b958b15a4 100644 --- a/lib/src/material/material_controls.dart +++ b/lib/src/material/material_controls.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:chewie/src/center_play_button.dart'; import 'package:chewie/src/center_seek_button.dart'; @@ -645,9 +646,28 @@ class _MaterialControlsState extends State 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 buffer = value.buffered.lastOrNull?.end.inMilliseconds ?? -1; + final int position = value.position.inMilliseconds; + + buffering = position >= buffer; + } else { + // -> No buffering + buffering = false; + } + } else { + buffering = controller.value.isBuffering; + } + // 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 +678,7 @@ class _MaterialControlsState extends State _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..8303580e7 100644 --- a/lib/src/material/material_desktop_controls.dart +++ b/lib/src/material/material_desktop_controls.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:chewie/src/animated_play_pause.dart'; import 'package:chewie/src/center_play_button.dart'; @@ -581,9 +582,28 @@ class _MaterialDesktopControlsState extends State 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 buffer = value.buffered.lastOrNull?.end.inMilliseconds ?? -1; + final int position = value.position.inMilliseconds; + + buffering = position >= buffer; + } else { + // -> No buffering + buffering = false; + } + } else { + buffering = controller.value.isBuffering; + } + // 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 +614,7 @@ class _MaterialDesktopControlsState extends State _displayBufferingIndicator = false; } } else { - _displayBufferingIndicator = controller.value.isBuffering; + _displayBufferingIndicator = buffering; } setState(() { From ecea26043c870c830a6754583975cdc5e9dcfb18 Mon Sep 17 00:00:00 2001 From: timoxd7 Date: Wed, 2 Apr 2025 11:56:00 +0200 Subject: [PATCH 2/9] Workaround loading spinner after video finished not disappearing on android --- lib/src/cupertino/cupertino_controls.dart | 13 +++++++++++-- lib/src/material/material_controls.dart | 13 +++++++++++-- lib/src/material/material_desktop_controls.dart | 13 +++++++++++-- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/lib/src/cupertino/cupertino_controls.dart b/lib/src/cupertino/cupertino_controls.dart index 4e12ee0c4..4590f4f77 100644 --- a/lib/src/cupertino/cupertino_controls.dart +++ b/lib/src/cupertino/cupertino_controls.dart @@ -818,10 +818,19 @@ class _CupertinoControlsState extends State // -> 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 buffer = value.buffered.lastOrNull?.end.inMilliseconds ?? -1; + final int position = value.position.inMilliseconds; - buffering = position >= buffer; + // 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; diff --git a/lib/src/material/material_controls.dart b/lib/src/material/material_controls.dart index b958b15a4..126be36fc 100644 --- a/lib/src/material/material_controls.dart +++ b/lib/src/material/material_controls.dart @@ -653,10 +653,19 @@ class _MaterialControlsState extends State // -> 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 buffer = value.buffered.lastOrNull?.end.inMilliseconds ?? -1; + final int position = value.position.inMilliseconds; - buffering = position >= buffer; + // 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; diff --git a/lib/src/material/material_desktop_controls.dart b/lib/src/material/material_desktop_controls.dart index 8303580e7..a165a3ca5 100644 --- a/lib/src/material/material_desktop_controls.dart +++ b/lib/src/material/material_desktop_controls.dart @@ -589,10 +589,19 @@ class _MaterialDesktopControlsState extends State // -> 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 buffer = value.buffered.lastOrNull?.end.inMilliseconds ?? -1; + final int position = value.position.inMilliseconds; - buffering = position >= buffer; + // 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; From 45c7fca689f495ec064d2b379a6a57ee7671fb1b Mon Sep 17 00:00:00 2001 From: timoxd7 Date: Mon, 7 Apr 2025 14:37:10 +0200 Subject: [PATCH 3/9] Moved buffer detection logic for andorid into shared function --- lib/src/cupertino/cupertino_controls.dart | 29 +---------------- lib/src/helpers/utils.dart | 31 +++++++++++++++++++ lib/src/material/material_controls.dart | 29 +---------------- .../material/material_desktop_controls.dart | 29 +---------------- 4 files changed, 34 insertions(+), 84 deletions(-) diff --git a/lib/src/cupertino/cupertino_controls.dart b/lib/src/cupertino/cupertino_controls.dart index 4590f4f77..b0488a275 100644 --- a/lib/src/cupertino/cupertino_controls.dart +++ b/lib/src/cupertino/cupertino_controls.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'dart:math' as math; import 'dart:ui' as ui; @@ -811,33 +810,7 @@ class _CupertinoControlsState extends State 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; - } + final bool buffering = getIsBuffering(controller); // display the progress bar indicator only after the buffering delay if it has been set if (chewieController.progressIndicatorDelay != null) { diff --git a/lib/src/helpers/utils.dart b/lib/src/helpers/utils.dart index 0f3a2e128..ab2a570e9 100644 --- a/lib/src/helpers/utils.dart +++ b/lib/src/helpers/utils.dart @@ -1,3 +1,7 @@ +import 'dart:io'; + +import 'package:video_player/video_player.dart'; + String formatDuration(Duration position) { final ms = position.inMilliseconds; @@ -30,3 +34,30 @@ String formatDuration(Duration position) { return formattedTime; } + +bool getIsBuffering(VideoPlayerController controller) { + final VideoPlayerValue value = controller.value; + + if (Platform.isAndroid) { + 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 126be36fc..3ea67f31d 100644 --- a/lib/src/material/material_controls.dart +++ b/lib/src/material/material_controls.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'package:chewie/src/center_play_button.dart'; import 'package:chewie/src/center_seek_button.dart'; @@ -646,33 +645,7 @@ class _MaterialControlsState extends State 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; - } + final bool buffering = getIsBuffering(controller); // display the progress bar indicator only after the buffering delay if it has been set if (chewieController.progressIndicatorDelay != null) { diff --git a/lib/src/material/material_desktop_controls.dart b/lib/src/material/material_desktop_controls.dart index a165a3ca5..ebffd9560 100644 --- a/lib/src/material/material_desktop_controls.dart +++ b/lib/src/material/material_desktop_controls.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'package:chewie/src/animated_play_pause.dart'; import 'package:chewie/src/center_play_button.dart'; @@ -582,33 +581,7 @@ class _MaterialDesktopControlsState extends State 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; - } + final bool buffering = getIsBuffering(controller); // display the progress bar indicator only after the buffering delay if it has been set if (chewieController.progressIndicatorDelay != null) { From 7eacc09f33f0cf4b6a7197bbb3e3cf4a320f3ea7 Mon Sep 17 00:00:00 2001 From: timoxd7 Date: Mon, 7 Apr 2025 14:52:32 +0200 Subject: [PATCH 4/9] Added workaround information for android to README --- README.md | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 72884a3be..4f3528dd6 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](#-android-warning-) +12. πŸ“± [iOS warning](#-ios-warning-) ## 🚨 IMPORTANT!!! (READ THIS FIRST) @@ -286,6 +287,33 @@ 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; + + // ... + } +``` + ## πŸ“± 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` From 0750c0e9441c1f2e5e1d94ae4eb3b7e48b20f773 Mon Sep 17 00:00:00 2001 From: timoxd7 Date: Mon, 7 Apr 2025 15:08:06 +0200 Subject: [PATCH 5/9] Added additional instructions for android buffering workaround --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 4f3528dd6..134b0084b 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,15 @@ Add the following to partly fix this behavior: } ``` +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` From 4474b316519caadf84474e3910d797aa87f308ad Mon Sep 17 00:00:00 2001 From: timoxd7 Date: Mon, 7 Apr 2025 15:16:55 +0200 Subject: [PATCH 6/9] Removed usage of dart:io --- lib/src/helpers/utils.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/src/helpers/utils.dart b/lib/src/helpers/utils.dart index ab2a570e9..4b8a12fab 100644 --- a/lib/src/helpers/utils.dart +++ b/lib/src/helpers/utils.dart @@ -1,5 +1,4 @@ -import 'dart:io'; - +import 'package:flutter/foundation.dart'; import 'package:video_player/video_player.dart'; String formatDuration(Duration position) { @@ -38,7 +37,7 @@ String formatDuration(Duration position) { bool getIsBuffering(VideoPlayerController controller) { final VideoPlayerValue value = controller.value; - if (Platform.isAndroid) { + 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. From 3515ca1846cd3a5ef267a50c401cbb006e4b233a Mon Sep 17 00:00:00 2001 From: timoxd7 Date: Mon, 7 Apr 2025 15:20:12 +0200 Subject: [PATCH 7/9] Fixed README links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 134b0084b..a6bcb1e4b 100644 --- a/README.md +++ b/README.md @@ -24,8 +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. ⚠️ [Android warning](#-android-warning-) -12. πŸ“± [iOS warning](#-ios-warning-) +11. ⚠️ [Android warning](#%EF%B8%8F-android-warning) +12. πŸ“± [iOS warning](#-ios-warning) ## 🚨 IMPORTANT!!! (READ THIS FIRST) From a01ef8719a781ed3acba971112f15cefd2e9be46 Mon Sep 17 00:00:00 2001 From: timoxd7 Date: Mon, 7 Apr 2025 16:04:06 +0200 Subject: [PATCH 8/9] Added information about getIsBuffering --- lib/src/helpers/utils.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/src/helpers/utils.dart b/lib/src/helpers/utils.dart index 4b8a12fab..26ae07b7f 100644 --- a/lib/src/helpers/utils.dart +++ b/lib/src/helpers/utils.dart @@ -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. bool getIsBuffering(VideoPlayerController controller) { final VideoPlayerValue value = controller.value; From 85cddc568736e25b29b76516d3b708eef4210420 Mon Sep 17 00:00:00 2001 From: timoxd7 Date: Tue, 8 Apr 2025 10:20:29 +0200 Subject: [PATCH 9/9] Updated comment --- lib/src/helpers/utils.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/src/helpers/utils.dart b/lib/src/helpers/utils.dart index 26ae07b7f..17ae2161a 100644 --- a/lib/src/helpers/utils.dart +++ b/lib/src/helpers/utils.dart @@ -34,11 +34,15 @@ 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. +/// 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 #912 for more details. +/// 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;