diff --git a/example/lib/app/app.dart b/example/lib/app/app.dart index 9900318cd..1e5cd37f7 100644 --- a/example/lib/app/app.dart +++ b/example/lib/app/app.dart @@ -41,7 +41,7 @@ class _ChewieDemoState extends State { Future initializePlayer() async { _videoPlayerController1 = VideoPlayerController.network( - 'https://assets.mixkit.co/videos/preview/mixkit-daytime-city-traffic-aerial-view-56-large.mp4'); + 'https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4'); _videoPlayerController2 = VideoPlayerController.network( 'https://assets.mixkit.co/videos/preview/mixkit-a-girl-blowing-a-bubble-gum-at-an-amusement-park-1226-large.mp4'); await Future.wait([ @@ -52,6 +52,16 @@ class _ChewieDemoState extends State { videoPlayerController: _videoPlayerController1, autoPlay: true, looping: true, + resolutions: { + "480p": + "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4", + "640p": + "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_640_3MG.mp4", + "1280p": + "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_1280_10MG.mp4", + "1920p": + "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_1920_18MG.mp4" + }, subtitle: Subtitles([ Subtitle( index: 0, @@ -139,6 +149,16 @@ class _ChewieDemoState extends State { videoPlayerController: _videoPlayerController1, autoPlay: true, looping: true, + resolutions: { + "480p": + "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4", + "640p": + "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_640_3MG.mp4", + "1280p": + "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_1280_10MG.mp4", + "1920p": + "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_1920_18MG.mp4" + }, subtitle: Subtitles([ Subtitle( index: 0, diff --git a/lib/src/chewie_player.dart b/lib/src/chewie_player.dart index 9f114ebf0..fe37d9806 100644 --- a/lib/src/chewie_player.dart +++ b/lib/src/chewie_player.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:chewie/src/chewie_progress_colors.dart'; import 'package:chewie/src/material/models/options_translation.dart'; @@ -14,6 +15,9 @@ import 'package:chewie/src/models/subtitle_model.dart'; import 'material/models/option_item.dart'; +import 'models/video_player_controller_extension.dart' + show VideoPlayerControllerExtension; + typedef ChewieRoutePageBuilder = Widget Function( BuildContext context, Animation animation, @@ -42,13 +46,19 @@ class Chewie extends StatefulWidget { class ChewieState extends State { bool _isFullScreen = false; - late PlayerNotifier notifier; + late _ChewieControllerProvider controllerProvider; @override void initState() { super.initState(); widget.controller.addListener(listener); - notifier = PlayerNotifier.init(); + controllerProvider = _ChewieControllerProvider( + controller: widget.controller, + child: ChangeNotifierProvider.value( + value: PlayerNotifier(), + builder: (context, w) => const PlayerWithControls(), + ), + ); } @override @@ -77,13 +87,7 @@ class ChewieState extends State { @override Widget build(BuildContext context) { - return _ChewieControllerProvider( - controller: widget.controller, - child: ChangeNotifierProvider.value( - value: notifier, - builder: (context, w) => const PlayerWithControls(), - ), - ); + return controllerProvider; } Widget _buildFullScreenVideo( @@ -120,14 +124,6 @@ class ChewieState extends State { Animation animation, Animation secondaryAnimation, ) { - final controllerProvider = _ChewieControllerProvider( - controller: widget.controller, - child: ChangeNotifierProvider.value( - value: notifier, - builder: (context, w) => const PlayerWithControls(), - ), - ); - if (widget.controller.routePageBuilder == null) { return _defaultRoutePageBuilder( context, animation, secondaryAnimation, controllerProvider); @@ -221,7 +217,7 @@ class ChewieState extends State { class ChewieController extends ChangeNotifier { ChewieController({ required this.videoPlayerController, - this.optionsTranslation, + this.resolutions, this.aspectRatio, this.autoInitialize = false, this.autoPlay = false, @@ -234,6 +230,7 @@ class ChewieController extends ChangeNotifier { this.overlay, this.showControlsOnInitialize = true, this.showOptions = true, + this.optionsTranslation, this.optionsBuilder, this.additionalOptions, this.showControls = true, @@ -259,6 +256,7 @@ class ChewieController extends ChangeNotifier { ChewieController copyWith({ VideoPlayerController? videoPlayerController, + Map? resolutions, OptionsTranslation? optionsTranslation, double? aspectRatio, bool? autoInitialize, @@ -296,6 +294,7 @@ class ChewieController extends ChangeNotifier { return ChewieController( videoPlayerController: videoPlayerController ?? this.videoPlayerController, + resolutions: resolutions ?? this.resolutions, optionsTranslation: optionsTranslation ?? this.optionsTranslation, aspectRatio: aspectRatio ?? this.aspectRatio, autoInitialize: autoInitialize ?? this.autoInitialize, @@ -339,11 +338,24 @@ class ChewieController extends ChangeNotifier { ); } + /// Set your custom resolutions here like for example: + /// ```dart + /// { + /// '480p': 'video_480p.mp4', + /// '720p': 'video_720p.mp4', + /// '1080p': 'video_1080p.mp4', + /// } + /// ``` + /// + /// Default: `null` -> resolutions/quality button will be hidden + final Map? resolutions; + /// If false, the options button in MaterialUI and MaterialDesktopUI /// won't be shown. final bool showOptions; /// Pass your translations for the options like: + /// - Resolution /// - PlaybackSpeed /// - Subtitles /// - Cancel @@ -370,7 +382,7 @@ class ChewieController extends ChangeNotifier { Subtitles? subtitle; /// The controller for the video you want to play - final VideoPlayerController videoPlayerController; + VideoPlayerController videoPlayerController; /// Initialize the Video on Startup. This will prep the video for playback. final bool autoInitialize; @@ -469,7 +481,9 @@ class ChewieController extends ChangeNotifier { bool get isPlaying => videoPlayerController.value.isPlaying; - Future _initialize() async { + Future _initialize({ + Duration? continueAt, + }) async { await videoPlayerController.setLooping(looping); if ((autoInitialize || autoPlay) && @@ -485,8 +499,12 @@ class ChewieController extends ChangeNotifier { await videoPlayerController.play(); } - if (startAt != null) { - await videoPlayerController.seekTo(startAt!); + if (continueAt != null) { + await videoPlayerController.seekTo(continueAt); + } else { + if (startAt != null) { + await videoPlayerController.seekTo(startAt!); + } } if (fullScreenByDefault) { @@ -544,6 +562,28 @@ class ChewieController extends ChangeNotifier { void setSubtitle(List newSubtitle) { subtitle = Subtitles(newSubtitle); } + + Future setResolution(String url) async { + final position = await videoPlayerController.position; + + switch (videoPlayerController.dataSourceType) { + case DataSourceType.asset: + videoPlayerController = + videoPlayerController.copyWithAsset(dataSource: url); + break; + case DataSourceType.file: + videoPlayerController = videoPlayerController.copyWithFile( + file: File.fromUri(Uri.parse(url))); + break; + case DataSourceType.network: + videoPlayerController = + videoPlayerController.copyWithNetwork(dataSource: url); + break; + default: + } + + await _initialize(continueAt: position); + } } class _ChewieControllerProvider extends InheritedWidget { @@ -556,6 +596,5 @@ class _ChewieControllerProvider extends InheritedWidget { final ChewieController controller; @override - bool updateShouldNotify(_ChewieControllerProvider old) => - controller != old.controller; + bool updateShouldNotify(_ChewieControllerProvider old) => true; } diff --git a/lib/src/material/material_controls.dart b/lib/src/material/material_controls.dart index 951d540b3..9cfcd46ae 100644 --- a/lib/src/material/material_controls.dart +++ b/lib/src/material/material_controls.dart @@ -9,11 +9,13 @@ import 'package:chewie/src/material/models/option_item.dart'; import 'package:chewie/src/material/widgets/options_dialog.dart'; import 'package:chewie/src/notifiers/index.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; import 'package:video_player/video_player.dart'; import 'package:chewie/src/models/subtitle_model.dart'; import 'widgets/playback_speed_dialog.dart'; +import 'widgets/resolution_dialog.dart'; class MaterialControls extends StatefulWidget { const MaterialControls({Key? key}) : super(key: key); @@ -159,7 +161,7 @@ class _MaterialControlsState extends State iconData: Icons.speed, title: chewieController.optionsTranslation?.playbackSpeedButtonText ?? 'Playback speed', - ) + ), ]; if (chewieController.subtitle != null && @@ -179,6 +181,21 @@ class _MaterialControlsState extends State ); } + if (chewieController.resolutions != null && + chewieController.resolutions!.isNotEmpty) { + options.add( + OptionItem( + onTap: () { + Navigator.pop(context); + _onResolutionTap(); + }, + iconData: Icons.settings, + title: chewieController.optionsTranslation?.resolutionButtonText ?? + 'Resolution', + ), + ); + } + if (chewieController.additionalOptions != null && chewieController.additionalOptions!(context).isNotEmpty) { options.addAll(chewieController.additionalOptions!(context)); @@ -442,6 +459,37 @@ class _MaterialControlsState extends State }); } + Future _onResolutionTap() async { + _hideTimer?.cancel(); + + final choosenResolution = await showModalBottomSheet( + context: context, + isScrollControlled: true, + useRootNavigator: true, + builder: (context) => ResolutionDialog( + reslutions: chewieController.resolutions!, + selectedResolution: notifier.selectedResolution, + cancelButtonText: chewieController.optionsTranslation?.cancelButtonText, + ), + ); + + if (choosenResolution != null) { + notifier.selectedResolution = choosenResolution; + + await chewieController + .setResolution(chewieController.resolutions![choosenResolution]!); + } + + if (_latestValue.isPlaying) { + _startHideTimer(); + } + SchedulerBinding.instance!.addPostFrameCallback((_) { + if (mounted) { + setState(() {}); + } + }); + } + void _cancelAndRestartTimer() { _hideTimer?.cancel(); _startHideTimer(); diff --git a/lib/src/material/models/options_translation.dart b/lib/src/material/models/options_translation.dart index ccbe97654..7bfd0e9c7 100644 --- a/lib/src/material/models/options_translation.dart +++ b/lib/src/material/models/options_translation.dart @@ -1,20 +1,24 @@ class OptionsTranslation { OptionsTranslation({ + this.resolutionButtonText, this.playbackSpeedButtonText, this.subtitlesButtonText, this.cancelButtonText, }); + String? resolutionButtonText; String? playbackSpeedButtonText; String? subtitlesButtonText; String? cancelButtonText; OptionsTranslation copyWith({ + String? resolutionButtonText, String? playbackSpeedButtonText, String? subtitlesButtonText, String? cancelButtonText, }) { return OptionsTranslation( + resolutionButtonText: resolutionButtonText ?? this.resolutionButtonText, playbackSpeedButtonText: playbackSpeedButtonText ?? this.playbackSpeedButtonText, subtitlesButtonText: subtitlesButtonText ?? this.subtitlesButtonText, @@ -24,13 +28,14 @@ class OptionsTranslation { @override String toString() => - 'OptionsTranslation(playbackSpeedButtonText: $playbackSpeedButtonText, subtitlesButtonText: $subtitlesButtonText, cancelButtonText: $cancelButtonText)'; + 'OptionsTranslation(resolutionButtonText: $resolutionButtonText, playbackSpeedButtonText: $playbackSpeedButtonText, subtitlesButtonText: $subtitlesButtonText, cancelButtonText: $cancelButtonText)'; @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is OptionsTranslation && + other.resolutionButtonText == resolutionButtonText && other.playbackSpeedButtonText == playbackSpeedButtonText && other.subtitlesButtonText == subtitlesButtonText && other.cancelButtonText == cancelButtonText; @@ -38,6 +43,7 @@ class OptionsTranslation { @override int get hashCode => + resolutionButtonText.hashCode ^ playbackSpeedButtonText.hashCode ^ subtitlesButtonText.hashCode ^ cancelButtonText.hashCode; diff --git a/lib/src/material/widgets/resolution_dialog.dart b/lib/src/material/widgets/resolution_dialog.dart new file mode 100644 index 000000000..58102c4ef --- /dev/null +++ b/lib/src/material/widgets/resolution_dialog.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +class ResolutionDialog extends StatefulWidget { + const ResolutionDialog({ + Key? key, + required this.reslutions, + this.selectedResolution, + this.cancelButtonText, + }) : super(key: key); + + final Map reslutions; + final String? selectedResolution; + final String? cancelButtonText; + + @override + _ResolutionDialogState createState() => _ResolutionDialogState(); +} + +class _ResolutionDialogState extends State { + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListView.builder( + shrinkWrap: true, + itemCount: widget.reslutions.length, + itemBuilder: (context, i) { + final item = widget.reslutions.entries.elementAt(i); + + return ListTile( + onTap: () => Navigator.pop(context, item.key), + leading: widget.selectedResolution != null + ? widget.selectedResolution! == item.key + ? const Icon(Icons.done) + : null + : null, + title: Text(item.key), + ); + }, + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Divider( + thickness: 1.0, + ), + ), + ListTile( + onTap: () => Navigator.pop(context), + leading: const Icon(Icons.close), + title: Text( + widget.cancelButtonText ?? 'Cancel', + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/models/video_player_controller_extension.dart b/lib/src/models/video_player_controller_extension.dart new file mode 100644 index 000000000..b902b7642 --- /dev/null +++ b/lib/src/models/video_player_controller_extension.dart @@ -0,0 +1,47 @@ +import 'dart:io'; + +import 'package:video_player/video_player.dart'; + +extension VideoPlayerControllerExtension on VideoPlayerController { + VideoPlayerController copyWithAsset({ + String? dataSource, + String? package, + Future? closedCaptionFile, + VideoPlayerOptions? videoPlayerOptions, + }) { + return VideoPlayerController.asset( + dataSource ?? this.dataSource, + package: package ?? this.package, + closedCaptionFile: closedCaptionFile ?? this.closedCaptionFile, + videoPlayerOptions: videoPlayerOptions ?? this.videoPlayerOptions, + ); + } + + VideoPlayerController copyWithFile({ + File? file, + Future? closedCaptionFile, + VideoPlayerOptions? videoPlayerOptions, + }) { + return VideoPlayerController.file( + file ?? File.fromUri(Uri.parse(dataSource)), + closedCaptionFile: closedCaptionFile ?? this.closedCaptionFile, + videoPlayerOptions: videoPlayerOptions ?? this.videoPlayerOptions, + ); + } + + VideoPlayerController copyWithNetwork({ + String? dataSource, + VideoFormat? formatHint, + Future? closedCaptionFile, + VideoPlayerOptions? videoPlayerOptions, + Map? httpHeaders, + }) { + return VideoPlayerController.network( + dataSource ?? this.dataSource, + formatHint: formatHint ?? this.formatHint, + closedCaptionFile: closedCaptionFile ?? this.closedCaptionFile, + videoPlayerOptions: videoPlayerOptions ?? this.videoPlayerOptions, + httpHeaders: httpHeaders ?? this.httpHeaders, + ); + } +} diff --git a/lib/src/notifiers/player_notifier.dart b/lib/src/notifiers/player_notifier.dart index dbba4faac..c21816a34 100644 --- a/lib/src/notifiers/player_notifier.dart +++ b/lib/src/notifiers/player_notifier.dart @@ -6,11 +6,9 @@ import 'package:flutter/material.dart'; /// over all State-Changes inside chewie /// class PlayerNotifier extends ChangeNotifier { - PlayerNotifier._( - bool hideStuff, - ) : _hideStuff = hideStuff; + bool _hideStuff = true; - bool _hideStuff; + String? _selectedResolution; bool get hideStuff => _hideStuff; @@ -19,10 +17,10 @@ class PlayerNotifier extends ChangeNotifier { notifyListeners(); } - // ignore: prefer_constructors_over_static_methods - static PlayerNotifier init() { - return PlayerNotifier._( - true, - ); + String? get selectedResolution => _selectedResolution; + + set selectedResolution(String? value) { + _selectedResolution = value; + notifyListeners(); } }