diff --git a/example/lib/app/app.dart b/example/lib/app/app.dart index 38840241b..41175ad78 100644 --- a/example/lib/app/app.dart +++ b/example/lib/app/app.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:chewie/chewie.dart'; import 'package:chewie_example/app/theme.dart'; import 'package:flutter/material.dart'; @@ -23,6 +25,7 @@ class _ChewieDemoState extends State { late VideoPlayerController _videoPlayerController1; late VideoPlayerController _videoPlayerController2; ChewieController? _chewieController; + int? bufferDelay; @override void initState() { @@ -39,6 +42,7 @@ class _ChewieDemoState extends State { } List srcs = [ + "https://assets.mixkit.co/videos/preview/mixkit-spinning-around-the-earth-29351-large.mp4", "https://assets.mixkit.co/videos/preview/mixkit-daytime-city-traffic-aerial-view-56-large.mp4", "https://assets.mixkit.co/videos/preview/mixkit-a-girl-blowing-a-bubble-gum-at-an-amusement-park-1226-large.mp4" ]; @@ -110,6 +114,8 @@ class _ChewieDemoState extends State { videoPlayerController: _videoPlayerController1, autoPlay: true, looping: true, + progressIndicatorDelay: + bufferDelay != null ? Duration(milliseconds: bufferDelay!) : null, additionalOptions: (context) { return [ @@ -155,7 +161,10 @@ class _ChewieDemoState extends State { Future toggleVideo() async { await _videoPlayerController1.pause(); - currPlayIndex = currPlayIndex == 0 ? 1 : 0; + currPlayIndex += 1; + if (currPlayIndex >= srcs.length) { + currPlayIndex = 0; + } await initializePlayer(); } @@ -302,9 +311,74 @@ class _ChewieDemoState extends State { ), ], ), + if (Platform.isAndroid) + ListTile( + title: const Text("Delay"), + subtitle: DelaySlider( + delay: + _chewieController?.progressIndicatorDelay?.inMilliseconds, + onSave: (delay) async { + if (delay != null) { + bufferDelay = delay == 0 ? null : delay; + await initializePlayer(); + } + }, + ), + ) ], ), ), ); } } + +class DelaySlider extends StatefulWidget { + const DelaySlider({Key? key, required this.delay, required this.onSave}) + : super(key: key); + + final int? delay; + final void Function(int?) onSave; + @override + State createState() => _DelaySliderState(); +} + +class _DelaySliderState extends State { + int? delay; + bool saved = false; + + @override + void initState() { + super.initState(); + delay = widget.delay; + } + + @override + Widget build(BuildContext context) { + const int max = 1000; + return ListTile( + title: Text( + "Progress indicator delay ${delay != null ? "${delay.toString()} MS" : ""}", + ), + subtitle: Slider( + value: delay != null ? (delay! / max) : 0, + onChanged: (value) async { + delay = (value * max).toInt(); + setState(() { + saved = false; + }); + }, + ), + trailing: IconButton( + icon: const Icon(Icons.save), + onPressed: saved + ? null + : () { + widget.onSave(delay); + setState(() { + saved = true; + }); + }, + ), + ); + } +} diff --git a/lib/src/chewie_player.dart b/lib/src/chewie_player.dart index 71eae87f1..99f861738 100644 --- a/lib/src/chewie_player.dart +++ b/lib/src/chewie_player.dart @@ -280,6 +280,7 @@ class ChewieController extends ChangeNotifier { this.systemOverlaysAfterFullScreen = SystemUiOverlay.values, this.deviceOrientationsAfterFullScreen = DeviceOrientation.values, this.routePageBuilder, + this.progressIndicatorDelay, this.hideControlsTimer = defaultHideControlsTimer, }) : assert( playbackSpeeds.every((speed) => speed > 0), @@ -324,6 +325,7 @@ class ChewieController extends ChangeNotifier { List? deviceOrientationsOnEnterFullScreen, List? systemOverlaysAfterFullScreen, List? deviceOrientationsAfterFullScreen, + Duration? progressIndicatorDelay, Widget Function( BuildContext, Animation, @@ -377,6 +379,8 @@ class ChewieController extends ChangeNotifier { this.deviceOrientationsAfterFullScreen, routePageBuilder: routePageBuilder ?? this.routePageBuilder, hideControlsTimer: hideControlsTimer ?? this.hideControlsTimer, + progressIndicatorDelay: + progressIndicatorDelay ?? this.progressIndicatorDelay, ); } @@ -513,6 +517,9 @@ class ChewieController extends ChangeNotifier { /// Defines a custom RoutePageBuilder for the fullscreen final ChewieRoutePageBuilder? routePageBuilder; + /// Defines a delay in milliseconds between entering buffering state and displaying the loading spinner. Set null (default) to disable it. + final Duration? progressIndicatorDelay; + static ChewieController of(BuildContext context) { final chewieControllerProvider = context.dependOnInheritedWidgetOfExactType()!; diff --git a/lib/src/cupertino/cupertino_controls.dart b/lib/src/cupertino/cupertino_controls.dart index a4e304ce8..47de2af17 100644 --- a/lib/src/cupertino/cupertino_controls.dart +++ b/lib/src/cupertino/cupertino_controls.dart @@ -47,6 +47,8 @@ class _CupertinoControlsState extends State bool _dragging = false; Duration? _subtitlesPosition; bool _subtitleOn = false; + Timer? _bufferingDisplayTimer; + bool _displayBufferingIndicator = false; late VideoPlayerController controller; @@ -91,7 +93,7 @@ class _CupertinoControlsState extends State absorbing: notifier.hideStuff, child: Stack( children: [ - if (_latestValue.isBuffering) + if (_displayBufferingIndicator) const Center( child: CircularProgressIndicator(), ) @@ -769,8 +771,32 @@ class _CupertinoControlsState extends State }); } + void _bufferingTimerTimeout() { + _displayBufferingIndicator = true; + if (mounted) { + setState(() {}); + } + } + void _updateState() { if (!mounted) return; + + // display the progress bar indicator only after the buffering delay if it has been set + if (chewieController.progressIndicatorDelay != null) { + if (controller.value.isBuffering) { + _bufferingDisplayTimer ??= Timer( + chewieController.progressIndicatorDelay!, + _bufferingTimerTimeout, + ); + } else { + _bufferingDisplayTimer?.cancel(); + _bufferingDisplayTimer = null; + _displayBufferingIndicator = false; + } + } else { + _displayBufferingIndicator = controller.value.isBuffering; + } + setState(() { _latestValue = controller.value; _subtitlesPosition = controller.value.position; diff --git a/lib/src/material/material_controls.dart b/lib/src/material/material_controls.dart index 4c409e4ca..8836a774e 100644 --- a/lib/src/material/material_controls.dart +++ b/lib/src/material/material_controls.dart @@ -40,6 +40,8 @@ class _MaterialControlsState extends State Timer? _showAfterExpandCollapseTimer; bool _dragging = false; bool _displayTapped = false; + Timer? _bufferingDisplayTimer; + bool _displayBufferingIndicator = false; final barHeight = 48.0 * 1.5; final marginSize = 5.0; @@ -82,7 +84,7 @@ class _MaterialControlsState extends State absorbing: notifier.hideStuff, child: Stack( children: [ - if (_latestValue.isBuffering) + if (_displayBufferingIndicator) const Center( child: CircularProgressIndicator(), ) @@ -550,8 +552,32 @@ class _MaterialControlsState extends State }); } + void _bufferingTimerTimeout() { + _displayBufferingIndicator = true; + if (mounted) { + setState(() {}); + } + } + void _updateState() { if (!mounted) return; + + // display the progress bar indicator only after the buffering delay if it has been set + if (chewieController.progressIndicatorDelay != null) { + if (controller.value.isBuffering) { + _bufferingDisplayTimer ??= Timer( + chewieController.progressIndicatorDelay!, + _bufferingTimerTimeout, + ); + } else { + _bufferingDisplayTimer?.cancel(); + _bufferingDisplayTimer = null; + _displayBufferingIndicator = false; + } + } else { + _displayBufferingIndicator = controller.value.isBuffering; + } + setState(() { _latestValue = controller.value; _subtitlesPosition = controller.value.position; diff --git a/lib/src/material/material_desktop_controls.dart b/lib/src/material/material_desktop_controls.dart index 2baa7cb22..f90326d11 100644 --- a/lib/src/material/material_desktop_controls.dart +++ b/lib/src/material/material_desktop_controls.dart @@ -41,6 +41,8 @@ class _MaterialDesktopControlsState extends State Timer? _showAfterExpandCollapseTimer; bool _dragging = false; bool _displayTapped = false; + Timer? _bufferingDisplayTimer; + bool _displayBufferingIndicator = false; final barHeight = 48.0 * 1.5; final marginSize = 5.0; @@ -83,7 +85,7 @@ class _MaterialDesktopControlsState extends State absorbing: notifier.hideStuff, child: Stack( children: [ - if (_latestValue.isBuffering) + if (_displayBufferingIndicator) const Center( child: CircularProgressIndicator(), ) @@ -530,8 +532,32 @@ class _MaterialDesktopControlsState extends State }); } + void _bufferingTimerTimeout() { + _displayBufferingIndicator = true; + if (mounted) { + setState(() {}); + } + } + void _updateState() { if (!mounted) return; + + // display the progress bar indicator only after the buffering delay if it has been set + if (chewieController.progressIndicatorDelay != null) { + if (controller.value.isBuffering) { + _bufferingDisplayTimer ??= Timer( + chewieController.progressIndicatorDelay!, + _bufferingTimerTimeout, + ); + } else { + _bufferingDisplayTimer?.cancel(); + _bufferingDisplayTimer = null; + _displayBufferingIndicator = false; + } + } else { + _displayBufferingIndicator = controller.value.isBuffering; + } + setState(() { _latestValue = controller.value; _subtitlesPosition = controller.value.position; diff --git a/lib/src/progress_bar.dart b/lib/src/progress_bar.dart index 1c6f51981..1cab09a2d 100644 --- a/lib/src/progress_bar.dart +++ b/lib/src/progress_bar.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:chewie/src/chewie_progress_colors.dart'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; @@ -81,7 +83,15 @@ class _VideoProgressBarState extends State { if (!controller.value.isInitialized) { return; } - _seekToRelativePosition(details.globalPosition); + // Should only seek if it's not running on Android, or if it is, + // then the VideoPlayerController cannot be buffering. + // On Android, we need to let the player buffer when scrolling + // in order to let the player buffer. https://github.com/flutter/flutter/issues/101409 + final shouldSeekToRelativePosition = + !Platform.isAndroid || !controller.value.isBuffering; + if (shouldSeekToRelativePosition) { + _seekToRelativePosition(details.globalPosition); + } widget.onDragUpdate?.call(); },