From fb87eb79533d0ce868599e6a3658ce8aad55304c Mon Sep 17 00:00:00 2001 From: Rebar Ahmad Date: Tue, 8 Jun 2021 05:24:12 +0200 Subject: [PATCH 1/2] =?UTF-8?q?add=20copyWith=20extension=20|=C2=A0add=20r?= =?UTF-8?q?esolution=20dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/lib/app/app.dart | 42 ++++++++-- lib/src/chewie_player.dart | 76 ++++++++++++++----- lib/src/material/material_controls.dart | 51 +++++++++++-- .../material/models/options_translation.dart | 8 +- .../material/widgets/resolution_dialog.dart | 60 +++++++++++++++ .../video_player_controller_extension.dart | 47 ++++++++++++ 6 files changed, 255 insertions(+), 29 deletions(-) create mode 100644 lib/src/material/widgets/resolution_dialog.dart create mode 100644 lib/src/models/video_player_controller_extension.dart diff --git a/example/lib/app/app.dart b/example/lib/app/app.dart index b31dc2a6e..4463661c6 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, @@ -140,6 +150,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, @@ -178,10 +198,20 @@ class _ChewieDemoState extends State { _videoPlayerController2.pause(); _videoPlayerController2.seekTo(const Duration()); _chewieController = ChewieController( - videoPlayerController: _videoPlayerController2, - autoPlay: true, - looping: true, - /* subtitle: Subtitles([ + videoPlayerController: _videoPlayerController2, + 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, start: Duration.zero, @@ -202,7 +232,7 @@ class _ChewieDemoState extends State { style: const TextStyle(color: Colors.white), ), ), */ - ); + ); }); }, child: const Padding( diff --git a/lib/src/chewie_player.dart b/lib/src/chewie_player.dart index 8b4f827e8..7e4fc97c3 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, @@ -43,12 +47,20 @@ 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: notifier, + builder: (context, w) => const PlayerWithControls(), + ), + ); } @override @@ -77,13 +89,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 +126,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 +219,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 +232,7 @@ class ChewieController extends ChangeNotifier { this.overlay, this.showControlsOnInitialize = true, this.showOptions = true, + this.optionsTranslation, this.optionsBuilder, this.additionalOptions, this.showControls = true, @@ -257,11 +256,27 @@ class ChewieController extends ChangeNotifier { _initialize(); } + /// Set your custom resolutions here like for example: + /// ```dart + /// { + /// '144p': 'video_144p.mp4', + /// '240p': 'video_240p.mp4', + /// '360p': 'video_360p.mp4', + /// '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 @@ -288,7 +303,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; @@ -462,6 +477,33 @@ class ChewieController extends ChangeNotifier { void setSubtitle(List newSubtitle) { subtitle = Subtitles(newSubtitle); } + + Future setResolution(String url) async { + final position = await videoPlayerController.position; + final wasPlayingBeforeChange = videoPlayerController.value.isPlaying; + await pause(); + 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 seekTo(position!); + if (wasPlayingBeforeChange) { + await play(); + } + notifyListeners(); + } } class _ChewieControllerProvider extends InheritedWidget { diff --git a/lib/src/material/material_controls.dart b/lib/src/material/material_controls.dart index a6577e198..34781533b 100644 --- a/lib/src/material/material_controls.dart +++ b/lib/src/material/material_controls.dart @@ -14,6 +14,7 @@ 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); @@ -32,6 +33,7 @@ class _MaterialControlsState extends State Timer? _initTimer; late var _subtitlesPosition = const Duration(); bool _subtitleOn = false; + String? _selectedResolution; Timer? _showAfterExpandCollapseTimer; bool _dragging = false; bool _displayTapped = false; @@ -77,10 +79,8 @@ class _MaterialControlsState extends State child: Stack( children: [ if (_latestValue.isBuffering) - const Expanded( - child: Center( - child: CircularProgressIndicator(), - ), + const Center( + child: CircularProgressIndicator(), ) else _buildHitArea(), @@ -161,7 +161,7 @@ class _MaterialControlsState extends State iconData: Icons.speed, title: chewieController.optionsTranslation?.playbackSpeedButtonText ?? 'Playback speed', - ) + ), ]; if (chewieController.subtitle != null && @@ -181,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)); @@ -444,6 +459,32 @@ 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: _selectedResolution, + cancelButtonText: chewieController.optionsTranslation?.cancelButtonText, + ), + ); + + if (choosenResolution != null) { + _selectedResolution = choosenResolution; + + chewieController + .setResolution(chewieController.resolutions![choosenResolution]!); + } + + if (_latestValue.isPlaying) { + _startHideTimer(); + } + } + 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, + ); + } +} From abaa40e81bec006ab38c7ca6c67b1379b364e709 Mon Sep 17 00:00:00 2001 From: Rebar Ahmad Date: Thu, 10 Jun 2021 23:59:08 +0200 Subject: [PATCH 2/2] =?UTF-8?q?add=20selectedResolution=20to=20notifier=20?= =?UTF-8?q?|=C2=A0fix=20Incorrect=20use=20of=20Parent=20Widget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/chewie_player.dart | 31 ++++++++----------- lib/src/cupertino/cupertino_controls.dart | 6 ++-- lib/src/material/material_controls.dart | 13 +++++--- .../material/material_desktop_controls.dart | 6 ++-- lib/src/notifiers/player_notifier.dart | 16 +++++----- 5 files changed, 33 insertions(+), 39 deletions(-) diff --git a/lib/src/chewie_player.dart b/lib/src/chewie_player.dart index 7e4fc97c3..a68304d71 100644 --- a/lib/src/chewie_player.dart +++ b/lib/src/chewie_player.dart @@ -46,18 +46,16 @@ 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: notifier, + value: PlayerNotifier(), builder: (context, w) => const PlayerWithControls(), ), ); @@ -259,9 +257,6 @@ class ChewieController extends ChangeNotifier { /// Set your custom resolutions here like for example: /// ```dart /// { - /// '144p': 'video_144p.mp4', - /// '240p': 'video_240p.mp4', - /// '360p': 'video_360p.mp4', /// '480p': 'video_480p.mp4', /// '720p': 'video_720p.mp4', /// '1080p': 'video_1080p.mp4', @@ -402,7 +397,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) && @@ -418,8 +415,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) { @@ -480,8 +481,7 @@ class ChewieController extends ChangeNotifier { Future setResolution(String url) async { final position = await videoPlayerController.position; - final wasPlayingBeforeChange = videoPlayerController.value.isPlaying; - await pause(); + switch (videoPlayerController.dataSourceType) { case DataSourceType.asset: videoPlayerController = @@ -498,11 +498,7 @@ class ChewieController extends ChangeNotifier { default: } - await seekTo(position!); - if (wasPlayingBeforeChange) { - await play(); - } - notifyListeners(); + await _initialize(continueAt: position); } } @@ -516,6 +512,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/cupertino/cupertino_controls.dart b/lib/src/cupertino/cupertino_controls.dart index 2867fece7..7c9da3abb 100644 --- a/lib/src/cupertino/cupertino_controls.dart +++ b/lib/src/cupertino/cupertino_controls.dart @@ -88,10 +88,8 @@ class _CupertinoControlsState extends State child: Stack( children: [ if (_latestValue.isBuffering) - const Expanded( - child: Center( - child: CircularProgressIndicator(), - ), + const Center( + child: CircularProgressIndicator(), ) else _buildHitArea(), diff --git a/lib/src/material/material_controls.dart b/lib/src/material/material_controls.dart index 34781533b..9cfcd46ae 100644 --- a/lib/src/material/material_controls.dart +++ b/lib/src/material/material_controls.dart @@ -9,6 +9,7 @@ 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'; @@ -33,7 +34,6 @@ class _MaterialControlsState extends State Timer? _initTimer; late var _subtitlesPosition = const Duration(); bool _subtitleOn = false; - String? _selectedResolution; Timer? _showAfterExpandCollapseTimer; bool _dragging = false; bool _displayTapped = false; @@ -468,21 +468,26 @@ class _MaterialControlsState extends State useRootNavigator: true, builder: (context) => ResolutionDialog( reslutions: chewieController.resolutions!, - selectedResolution: _selectedResolution, + selectedResolution: notifier.selectedResolution, cancelButtonText: chewieController.optionsTranslation?.cancelButtonText, ), ); if (choosenResolution != null) { - _selectedResolution = choosenResolution; + notifier.selectedResolution = choosenResolution; - chewieController + await chewieController .setResolution(chewieController.resolutions![choosenResolution]!); } if (_latestValue.isPlaying) { _startHideTimer(); } + SchedulerBinding.instance!.addPostFrameCallback((_) { + if (mounted) { + setState(() {}); + } + }); } void _cancelAndRestartTimer() { diff --git a/lib/src/material/material_desktop_controls.dart b/lib/src/material/material_desktop_controls.dart index fb41d58a4..404080cab 100644 --- a/lib/src/material/material_desktop_controls.dart +++ b/lib/src/material/material_desktop_controls.dart @@ -79,10 +79,8 @@ class _MaterialDesktopControlsState extends State child: Stack( children: [ if (_latestValue.isBuffering) - const Expanded( - child: Center( - child: CircularProgressIndicator(), - ), + const Center( + child: CircularProgressIndicator(), ) else _buildHitArea(), 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(); } }