From 3b747afdc5bcf8d5e5bb4170ff661a8357425d9a Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Thu, 13 Jun 2024 15:59:02 -0700 Subject: [PATCH 1/5] migrate from riiver_player to video_player --- .../src/thunder_video_player.dart | 164 ++++++++++++++---- macos/Flutter/GeneratedPluginRegistrant.swift | 4 +- pubspec.lock | 72 ++++---- pubspec.yaml | 2 +- 4 files changed, 177 insertions(+), 65 deletions(-) diff --git a/lib/utils/video_player/src/thunder_video_player.dart b/lib/utils/video_player/src/thunder_video_player.dart index a694229af..3c8ed50bc 100644 --- a/lib/utils/video_player/src/thunder_video_player.dart +++ b/lib/utils/video_player/src/thunder_video_player.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:river_player/river_player.dart'; +import 'package:video_player/video_player.dart'; import 'package:thunder/core/enums/internet_connection_type.dart'; import 'package:thunder/core/enums/video_auto_play.dart'; @@ -28,12 +28,11 @@ class ThunderVideoPlayer extends StatefulWidget { } class _ThunderVideoPlayerState extends State { - late BetterPlayerController _betterPlayerController; - late BetterPlayerDataSource _betterPlayerDataSource; + late VideoPlayerController _videoPlayerController; @override void dispose() async { - _betterPlayerController.dispose(); + _videoPlayerController.dispose(); super.dispose(); } @@ -45,6 +44,7 @@ class _ThunderVideoPlayerState extends State { bool autoPlayVideo(ThunderState thunderBloc) { final networkCubit = context.read().state; + if (thunderBloc.videoAutoPlay == VideoAutoPlay.always) { return true; } else if (thunderBloc.videoAutoPlay == VideoAutoPlay.onWifi && networkCubit.internetConnectionType == InternetConnectionType.wifi) { @@ -56,30 +56,18 @@ class _ThunderVideoPlayerState extends State { Future _initializePlayer() async { final thunderBloc = context.read().state; - _betterPlayerDataSource = BetterPlayerDataSource( - BetterPlayerDataSourceType.network, - widget.videoUrl, - ); - BetterPlayerConfiguration betterPlayerConfiguration = BetterPlayerConfiguration( - aspectRatio: 16 / 10, - fit: BoxFit.cover, - autoPlay: autoPlayVideo(thunderBloc), - fullScreenByDefault: thunderBloc.videoAutoFullscreen, - looping: thunderBloc.videoAutoLoop, - autoDetectFullscreenAspectRatio: true, - useRootNavigator: true, - autoDetectFullscreenDeviceOrientation: true, - autoDispose: true, + + _videoPlayerController = VideoPlayerController.networkUrl( + Uri.parse(widget.videoUrl), + videoPlayerOptions: VideoPlayerOptions(), ); - _betterPlayerController = BetterPlayerController(betterPlayerConfiguration); - _betterPlayerController - ..setupDataSource(_betterPlayerDataSource) - ..setVolume(thunderBloc.videoAutoMute ? 0 : 1) - ..setSpeed(thunderBloc.videoDefaultPlaybackSpeed.value); + _videoPlayerController.setVolume(thunderBloc.videoAutoMute ? 0 : 1); + _videoPlayerController.setPlaybackSpeed(thunderBloc.videoDefaultPlaybackSpeed.value); + _videoPlayerController.setLooping(thunderBloc.videoAutoLoop); - _betterPlayerController.addEventsListener((event) { - if (event.betterPlayerEventType == BetterPlayerEventType.exception) { + _videoPlayerController.addListener(() { + if (_videoPlayerController.value.hasError) { showSnackbar( l10n.failedToLoadVideo, trailingIcon: Icons.chevron_right_rounded, @@ -89,6 +77,17 @@ class _ThunderVideoPlayerState extends State { ); } }); + + _videoPlayerController.initialize().then( + (value) { + setState(() {}); + _videoPlayerController.play(); + + if (autoPlayVideo(thunderBloc)) { + _videoPlayerController.play(); + } + }, + ); } @override @@ -96,9 +95,6 @@ class _ThunderVideoPlayerState extends State { return Scaffold( backgroundColor: Colors.black, body: SafeArea( - bottom: false, - left: false, - right: false, child: Stack( children: [ Row( @@ -130,13 +126,121 @@ class _ThunderVideoPlayerState extends State { ), Center( child: AspectRatio( - aspectRatio: 16 / 10, - child: BetterPlayer(controller: _betterPlayerController), + aspectRatio: _videoPlayerController.value.aspectRatio, + child: Stack(children: [ + VideoPlayer(_videoPlayerController), + GestureDetector( + onTap: () { + if (_videoPlayerController.value.isPlaying) { + _videoPlayerController.pause(); + } else { + _videoPlayerController.play(); + } + + setState(() {}); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOutCubicEmphasized, + color: _videoPlayerController.value.isPlaying ? Colors.transparent : Colors.black.withOpacity(0.2), + ), + ), + ]), ), ), + VideoPlayerControls(controller: _videoPlayerController), ], ), ), ); } } + +class VideoPlayerControls extends StatefulWidget { + final VideoPlayerController controller; + + const VideoPlayerControls({super.key, required this.controller}); + + @override + State createState() => _VideoPlayerControlsState(); +} + +class _VideoPlayerControlsState extends State { + late VoidCallback listener; + + _VideoPlayerControlsState() { + listener = () { + if (!mounted) return; + setState(() {}); + }; + } + + @override + void initState() { + super.initState(); + widget.controller.addListener(listener); + } + + @override + void deactivate() { + widget.controller.removeListener(listener); + super.deactivate(); + } + + String formatTime(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, "0"); + + String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60).abs()); + String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60).abs()); + + if (duration.inHours > 0) { + return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds"; + } + + return "$twoDigitMinutes:$twoDigitSeconds"; + } + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.bottomCenter, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + onPressed: () { + widget.controller.value.isPlaying ? widget.controller.pause() : widget.controller.play(); + setState(() {}); + }, + icon: Icon( + widget.controller.value.isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, + color: Colors.white.withOpacity(0.90), + ), + ), + Text( + '${formatTime(widget.controller.value.position)} / ${formatTime(widget.controller.value.duration)}', + style: TextStyle(color: Colors.white.withOpacity(0.90)), + ), + ], + ), + Container( + height: 5.0, + margin: const EdgeInsets.only(left: 8.0, right: 8.0, bottom: 16.0), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(16.0)), + clipBehavior: Clip.hardEdge, + child: VideoProgressIndicator( + widget.controller, + allowScrubbing: true, + padding: EdgeInsets.zero, + ), + ), + ], + ), + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index e18dd18b7..a8eb234e8 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -18,7 +18,7 @@ import shared_preferences_foundation import sqflite import sqlite3_flutter_libs import url_launcher_macos -import wakelock_plus +import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) @@ -34,5 +34,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) + FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index bc401ad4d..befa35ba3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -806,14 +806,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_widget_from_html_core: - dependency: transitive - description: - name: flutter_widget_from_html_core - sha256: "028f4989b9ff4907466af233d50146d807772600d98a3e895662fbdb09c39225" - url: "https://pub.dev" - source: hosted - version: "0.14.11" freezed_annotation: dependency: transitive description: @@ -1470,14 +1462,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" - river_player: - dependency: "direct main" - description: - name: river_player - sha256: "0a64cd278a632abf4bbb39460a631a7d62d75f267a836967789bd2370df2dd9d" - url: "https://pub.dev" - source: hosted - version: "0.1.3" rxdart: dependency: transitive description: @@ -1931,38 +1915,62 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" - visibility_detector: + video_player: dependency: "direct main" description: - name: visibility_detector - sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + name: video_player + sha256: aced48e701e24c02b0b7f881a8819e4937794e46b5a5821005e2bf3b40a324cc url: "https://pub.dev" source: hosted - version: "0.4.0+2" - vm_service: + version: "2.8.7" + video_player_android: dependency: transitive description: - name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + name: video_player_android + sha256: "134e1ad410d67e18a19486ed9512c72dfc6d8ffb284d0e8f2e99e903d1ba8fa3" url: "https://pub.dev" source: hosted - version: "13.0.0" - wakelock_plus: + version: "2.4.14" + video_player_avfoundation: dependency: transitive description: - name: wakelock_plus - sha256: "104d94837bb28c735894dcd592877e990149c380e6358b00c04398ca1426eed4" + name: video_player_avfoundation + sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c url: "https://pub.dev" source: hosted - version: "1.2.1" - wakelock_plus_platform_interface: + version: "2.6.1" + video_player_platform_interface: dependency: transitive description: - name: wakelock_plus_platform_interface - sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" + name: video_player_platform_interface + sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "6.2.2" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: ff4d69a6614b03f055397c27a71c9d3ddea2b2a23d71b2ba0164f59ca32b8fe2 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + visibility_detector: + dependency: "direct main" + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 27c97fa1c..054ebe0e5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -107,9 +107,9 @@ dependencies: flutter_sharing_intent: ^1.1.1 drift: ^2.16.0 sqlite3_flutter_libs: ^0.5.20 - river_player: ^0.1.3 connectivity_plus: ^6.0.2 super_sliver_list: ^0.4.1 + video_player: ^2.8.7 dev_dependencies: From eab1f4c955cb77acff6dfb2747e13710903d4115 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Thu, 13 Jun 2024 17:02:05 -0700 Subject: [PATCH 2/5] added volume mute control, added playback speed --- .../src/thunder_video_player.dart | 79 +++++++++++++++---- 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/lib/utils/video_player/src/thunder_video_player.dart b/lib/utils/video_player/src/thunder_video_player.dart index 3c8ed50bc..bfd7ca269 100644 --- a/lib/utils/video_player/src/thunder_video_player.dart +++ b/lib/utils/video_player/src/thunder_video_player.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:thunder/core/enums/video_playback_speed.dart'; +import 'package:thunder/shared/thunder_popup_menu_item.dart'; import 'package:video_player/video_player.dart'; import 'package:thunder/core/enums/internet_connection_type.dart'; @@ -81,8 +84,6 @@ class _ThunderVideoPlayerState extends State { _videoPlayerController.initialize().then( (value) { setState(() {}); - _videoPlayerController.play(); - if (autoPlayVideo(thunderBloc)) { _videoPlayerController.play(); } @@ -208,23 +209,62 @@ class _VideoPlayerControlsState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.center, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - IconButton( - onPressed: () { - widget.controller.value.isPlaying ? widget.controller.pause() : widget.controller.play(); - setState(() {}); - }, - icon: Icon( - widget.controller.value.isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, - color: Colors.white.withOpacity(0.90), - ), + Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + onPressed: () { + widget.controller.value.isPlaying ? widget.controller.pause() : widget.controller.play(); + setState(() {}); + }, + icon: Icon( + widget.controller.value.isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, + color: Colors.white.withOpacity(0.90), + ), + ), + Text( + '${formatTime(widget.controller.value.position)} / ${formatTime(widget.controller.value.duration)}', + style: TextStyle(color: Colors.white.withOpacity(0.90)), + ), + ], ), - Text( - '${formatTime(widget.controller.value.position)} / ${formatTime(widget.controller.value.duration)}', - style: TextStyle(color: Colors.white.withOpacity(0.90)), + Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + onPressed: () { + widget.controller.value.volume == 0 ? widget.controller.setVolume(1) : widget.controller.setVolume(0); + setState(() {}); + }, + icon: Icon( + widget.controller.value.volume == 0 ? Icons.volume_mute_rounded : Icons.volume_up_rounded, + color: Colors.white.withOpacity(0.90), + ), + ), + PopupMenuButton( + itemBuilder: (context) => VideoPlayBackSpeed.values + .map( + (videoPlaybackSpeed) => ThunderPopupMenuItem( + onTap: () { + widget.controller.setPlaybackSpeed(videoPlaybackSpeed.value); + setState(() {}); + }, + icon: Icons.speed_rounded, + title: videoPlaybackSpeed.label, + ), + ) + .toList(), + icon: Icon( + Icons.speed_rounded, + color: Colors.white.withOpacity(0.90), + ), + ), + ], ), ], ), @@ -237,6 +277,11 @@ class _VideoPlayerControlsState extends State { widget.controller, allowScrubbing: true, padding: EdgeInsets.zero, + colors: const VideoProgressColors( + playedColor: Colors.white70, + bufferedColor: Colors.white12, + backgroundColor: Colors.white10, + ), ), ), ], From d506278b20dd312a77e77bf6b9e564140b664563 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Fri, 14 Jun 2024 14:03:09 -0700 Subject: [PATCH 3/5] added primitive method of setting video to fullscreen --- .../src/thunder_video_player.dart | 133 ++++++++++-------- macos/Podfile.lock | 11 +- 2 files changed, 84 insertions(+), 60 deletions(-) diff --git a/lib/utils/video_player/src/thunder_video_player.dart b/lib/utils/video_player/src/thunder_video_player.dart index bfd7ca269..2ffc9c960 100644 --- a/lib/utils/video_player/src/thunder_video_player.dart +++ b/lib/utils/video_player/src/thunder_video_player.dart @@ -33,6 +33,9 @@ class ThunderVideoPlayer extends StatefulWidget { class _ThunderVideoPlayerState extends State { late VideoPlayerController _videoPlayerController; + /// Used to toggle the fullscreen mode + bool isFullScreen = false; + @override void dispose() async { _videoPlayerController.dispose(); @@ -58,16 +61,16 @@ class _ThunderVideoPlayerState extends State { } Future _initializePlayer() async { - final thunderBloc = context.read().state; + final state = context.read().state; _videoPlayerController = VideoPlayerController.networkUrl( Uri.parse(widget.videoUrl), videoPlayerOptions: VideoPlayerOptions(), ); - _videoPlayerController.setVolume(thunderBloc.videoAutoMute ? 0 : 1); - _videoPlayerController.setPlaybackSpeed(thunderBloc.videoDefaultPlaybackSpeed.value); - _videoPlayerController.setLooping(thunderBloc.videoAutoLoop); + _videoPlayerController.setVolume(state.videoAutoMute ? 0 : 1); + _videoPlayerController.setPlaybackSpeed(state.videoDefaultPlaybackSpeed.value); + _videoPlayerController.setLooping(state.videoAutoLoop); _videoPlayerController.addListener(() { if (_videoPlayerController.value.hasError) { @@ -83,8 +86,11 @@ class _ThunderVideoPlayerState extends State { _videoPlayerController.initialize().then( (value) { - setState(() {}); - if (autoPlayVideo(thunderBloc)) { + setState(() { + isFullScreen = state.videoAutoFullscreen; + }); + + if (autoPlayVideo(state)) { _videoPlayerController.play(); } }, @@ -96,61 +102,64 @@ class _ThunderVideoPlayerState extends State { return Scaffold( backgroundColor: Colors.black, body: SafeArea( - child: Stack( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(4.0), - child: IconButton( - onPressed: () => Navigator.pop(context), - icon: Icon( - Icons.arrow_back, - semanticLabel: MaterialLocalizations.of(context).backButtonTooltip, - color: Colors.white.withOpacity(0.90), + child: RotatedBox( + quarterTurns: isFullScreen ? 0 : 1, + child: Stack( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(4.0), + child: IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon( + Icons.arrow_back, + semanticLabel: MaterialLocalizations.of(context).backButtonTooltip, + color: Colors.white.withOpacity(0.90), + ), ), ), - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: IconButton( - onPressed: () => handleLink(context, url: widget.videoUrl, forceOpenInBrowser: true), - icon: Icon( - Icons.open_in_browser_rounded, - semanticLabel: l10n.openInBrowser, - color: Colors.white.withOpacity(0.90), + Padding( + padding: const EdgeInsets.all(4.0), + child: IconButton( + onPressed: () => handleLink(context, url: widget.videoUrl, forceOpenInBrowser: true), + icon: Icon( + Icons.open_in_browser_rounded, + semanticLabel: l10n.openInBrowser, + color: Colors.white.withOpacity(0.90), + ), ), ), - ), - ], - ), - Center( - child: AspectRatio( - aspectRatio: _videoPlayerController.value.aspectRatio, - child: Stack(children: [ - VideoPlayer(_videoPlayerController), - GestureDetector( - onTap: () { - if (_videoPlayerController.value.isPlaying) { - _videoPlayerController.pause(); - } else { - _videoPlayerController.play(); - } + ], + ), + Center( + child: AspectRatio( + aspectRatio: _videoPlayerController.value.aspectRatio, + child: Stack(children: [ + VideoPlayer(_videoPlayerController), + GestureDetector( + onTap: () { + if (_videoPlayerController.value.isPlaying) { + _videoPlayerController.pause(); + } else { + _videoPlayerController.play(); + } - setState(() {}); - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOutCubicEmphasized, - color: _videoPlayerController.value.isPlaying ? Colors.transparent : Colors.black.withOpacity(0.2), + setState(() {}); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOutCubicEmphasized, + color: _videoPlayerController.value.isPlaying ? Colors.transparent : Colors.black.withOpacity(0.2), + ), ), - ), - ]), + ]), + ), ), - ), - VideoPlayerControls(controller: _videoPlayerController), - ], + VideoPlayerControls(controller: _videoPlayerController, onToggleFullScreen: () => setState(() => isFullScreen = !isFullScreen)), + ], + ), ), ), ); @@ -158,9 +167,13 @@ class _ThunderVideoPlayerState extends State { } class VideoPlayerControls extends StatefulWidget { + /// The [VideoPlayerController] that this widget is controlling final VideoPlayerController controller; - const VideoPlayerControls({super.key, required this.controller}); + /// Used to toggle the fullscreen mode + final VoidCallback onToggleFullScreen; + + const VideoPlayerControls({super.key, required this.controller, required this.onToggleFullScreen}); @override State createState() => _VideoPlayerControlsState(); @@ -264,6 +277,16 @@ class _VideoPlayerControlsState extends State { color: Colors.white.withOpacity(0.90), ), ), + IconButton( + onPressed: () { + widget.onToggleFullScreen(); + setState(() {}); + }, + icon: Icon( + Icons.fullscreen_rounded, + color: Colors.white.withOpacity(0.90), + ), + ), ], ), ], diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 048a9bbb4..1d7fc86fe 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -44,7 +44,8 @@ PODS: - sqlite3/rtree - url_launcher_macos (0.0.1): - FlutterMacOS - - wakelock_plus (0.0.1): + - video_player_avfoundation (0.0.1): + - Flutter - FlutterMacOS DEPENDENCIES: @@ -62,7 +63,7 @@ DEPENDENCIES: - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) + - video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`) SPEC REPOS: trunk: @@ -97,8 +98,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos - wakelock_plus: - :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos + video_player_avfoundation: + :path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin SPEC CHECKSUMS: connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db @@ -116,7 +117,7 @@ SPEC CHECKSUMS: sqlite3: 02d1f07eaaa01f80a1c16b4b31dfcbb3345ee01a sqlite3_flutter_libs: 06a05802529659a272beac4ee1350bfec294f386 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 - wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 PODFILE CHECKSUM: c2e95c8c0fe03c5c57e438583cae4cc732296009 From 8448b23447a0729e33e2ddcea76638962e852527 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Sun, 23 Jun 2024 16:19:16 -0700 Subject: [PATCH 4/5] hide system status bar when toggling full screen --- ios/Podfile.lock | 46 +++---------------- .../src/thunder_video_player.dart | 20 +++++++- 2 files changed, 24 insertions(+), 42 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b5547620a..5b60ab976 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,7 +1,6 @@ PODS: - background_fetch (1.3.2): - Flutter - - Cache (6.0.0) - connectivity_plus (0.0.1): - Flutter - FlutterMacOS @@ -32,12 +31,6 @@ PODS: - gal (1.0.0): - Flutter - FlutterMacOS - - GCDWebServer (3.5.4): - - GCDWebServer/Core (= 3.5.4) - - GCDWebServer/Core (3.5.4) - - HLSCachingReverseProxyServer (0.1.0): - - GCDWebServer (~> 3.5) - - PINCache (>= 3.0.1-beta.3) - image_picker_ios (0.0.1): - Flutter - OrderedSet (5.0.0) @@ -48,24 +41,10 @@ PODS: - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter - - PINCache (3.0.4): - - PINCache/Arc-exception-safe (= 3.0.4) - - PINCache/Core (= 3.0.4) - - PINCache/Arc-exception-safe (3.0.4): - - PINCache/Core - - PINCache/Core (3.0.4): - - PINOperation (~> 1.2.3) - - PINOperation (1.2.3) - pointer_interceptor_ios (0.0.1): - Flutter - push_ios (0.0.1): - Flutter - - river_player (0.0.1): - - Cache (~> 6.0.0) - - Flutter - - GCDWebServer - - HLSCachingReverseProxyServer - - PINCache - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -93,8 +72,9 @@ PODS: - Flutter - url_launcher_ios (0.0.1): - Flutter - - wakelock_plus (0.0.1): + - video_player_avfoundation (0.0.1): - Flutter + - FlutterMacOS - webview_flutter_wkwebview (0.0.1): - Flutter @@ -118,24 +98,18 @@ DEPENDENCIES: - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`) - push_ios (from `.symlinks/plugins/push_ios/ios`) - - river_player (from `.symlinks/plugins/river_player/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/darwin`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) SPEC REPOS: trunk: - - Cache - - GCDWebServer - - HLSCachingReverseProxyServer - OrderedSet - - PINCache - - PINOperation - sqlite3 EXTERNAL SOURCES: @@ -177,8 +151,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/pointer_interceptor_ios/ios" push_ios: :path: ".symlinks/plugins/push_ios/ios" - river_player: - :path: ".symlinks/plugins/river_player/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: @@ -191,14 +163,13 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/uni_links/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" - wakelock_plus: - :path: ".symlinks/plugins/wakelock_plus/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/darwin" webview_flutter_wkwebview: :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: background_fetch: 2319bf7e18237b4b269430b7f14d177c0df09c5a - Cache: 4ca7e00363fca5455f26534e5607634c820ffc2d connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 @@ -211,18 +182,13 @@ SPEC CHECKSUMS: flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98 gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 - GCDWebServer: 2c156a56c8226e2d5c0c3f208a3621ccffbe3ce4 - HLSCachingReverseProxyServer: 59935e1e0244ad7f3375d75b5ef46e8eb26ab181 image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 - PINCache: d9a87a0ff397acffe9e2f0db972ac14680441158 - PINOperation: fb563bcc9c32c26d6c78aaff967d405aa2ee74a7 pointer_interceptor_ios: 9280618c0b2eeb80081a343924aa8ad756c21375 push_ios: 2bd1b4d3f782209da1f571db1250af236957e807 - river_player: ba880eae2d34deaff38fdf53a96b63edc654c9bf share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec @@ -230,7 +196,7 @@ SPEC CHECKSUMS: sqlite3_flutter_libs: af0e8fe9bce48abddd1ffdbbf839db0302d72d80 uni_links: d97da20c7701486ba192624d99bffaaffcfc298a url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 - wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36 PODFILE CHECKSUM: 8d23d5c4d896af3a5f2a08e0206462ca9882e556 diff --git a/lib/utils/video_player/src/thunder_video_player.dart b/lib/utils/video_player/src/thunder_video_player.dart index 2ffc9c960..572c7aeb4 100644 --- a/lib/utils/video_player/src/thunder_video_player.dart +++ b/lib/utils/video_player/src/thunder_video_player.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -88,6 +89,9 @@ class _ThunderVideoPlayerState extends State { (value) { setState(() { isFullScreen = state.videoAutoFullscreen; + if (isFullScreen) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + } }); if (autoPlayVideo(state)) { @@ -103,7 +107,7 @@ class _ThunderVideoPlayerState extends State { backgroundColor: Colors.black, body: SafeArea( child: RotatedBox( - quarterTurns: isFullScreen ? 0 : 1, + quarterTurns: !isFullScreen ? 0 : 1, child: Stack( children: [ Row( @@ -157,7 +161,19 @@ class _ThunderVideoPlayerState extends State { ]), ), ), - VideoPlayerControls(controller: _videoPlayerController, onToggleFullScreen: () => setState(() => isFullScreen = !isFullScreen)), + VideoPlayerControls( + controller: _videoPlayerController, + onToggleFullScreen: () => setState( + () { + isFullScreen = !isFullScreen; + if (isFullScreen) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + } else { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } + }, + ), + ), ], ), ), From 7d95d379839f94892a19dc0c74c9f07ca943fe93 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Mon, 1 Jul 2024 13:12:18 -0700 Subject: [PATCH 5/5] hide video player controls automatically when playing --- .../src/thunder_video_player.dart | 117 ++++++++++++------ 1 file changed, 78 insertions(+), 39 deletions(-) diff --git a/lib/utils/video_player/src/thunder_video_player.dart b/lib/utils/video_player/src/thunder_video_player.dart index 572c7aeb4..c2f0251c9 100644 --- a/lib/utils/video_player/src/thunder_video_player.dart +++ b/lib/utils/video_player/src/thunder_video_player.dart @@ -1,6 +1,7 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/core/enums/video_playback_speed.dart'; @@ -34,11 +35,22 @@ class ThunderVideoPlayer extends StatefulWidget { class _ThunderVideoPlayerState extends State { late VideoPlayerController _videoPlayerController; + /// Used to toggle the video control visibility + bool isVideoControlsVisible = true; + + /// Used to debounce the video control visibility + Timer? debounceTimer; + + /// Timer for delaying the video control visibility + Timer? timer; + /// Used to toggle the fullscreen mode bool isFullScreen = false; @override void dispose() async { + timer?.cancel(); + debounceTimer?.cancel(); _videoPlayerController.dispose(); super.dispose(); } @@ -74,6 +86,18 @@ class _ThunderVideoPlayerState extends State { _videoPlayerController.setLooping(state.videoAutoLoop); _videoPlayerController.addListener(() { + if (_videoPlayerController.value.isPlaying && isVideoControlsVisible && timer?.isActive != true) { + timer = Timer(const Duration(seconds: 3), () { + // Hide video controls + setState(() => isVideoControlsVisible = false); + }); + } else if (!_videoPlayerController.value.isPlaying) { + timer?.cancel(); + + // Show video controls + if (!isVideoControlsVisible) setState(() => isVideoControlsVisible = true); + } + if (_videoPlayerController.value.hasError) { showSnackbar( l10n.failedToLoadVideo, @@ -110,33 +134,49 @@ class _ThunderVideoPlayerState extends State { quarterTurns: !isFullScreen ? 0 : 1, child: Stack( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(4.0), - child: IconButton( - onPressed: () => Navigator.pop(context), - icon: Icon( - Icons.arrow_back, - semanticLabel: MaterialLocalizations.of(context).backButtonTooltip, - color: Colors.white.withOpacity(0.90), + AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: isVideoControlsVisible ? 1.0 : 0.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(4.0), + child: IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon( + Icons.arrow_back, + semanticLabel: MaterialLocalizations.of(context).backButtonTooltip, + color: Colors.white.withOpacity(0.90), + ), ), ), - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: IconButton( - onPressed: () => handleLink(context, url: widget.videoUrl, forceOpenInBrowser: true), - icon: Icon( - Icons.open_in_browser_rounded, - semanticLabel: l10n.openInBrowser, - color: Colors.white.withOpacity(0.90), + Padding( + padding: const EdgeInsets.all(4.0), + child: IconButton( + onPressed: () => handleLink(context, url: widget.videoUrl, forceOpenInBrowser: true), + icon: Icon( + Icons.open_in_browser_rounded, + semanticLabel: l10n.openInBrowser, + color: Colors.white.withOpacity(0.90), + ), ), ), - ), - ], + ], + ), ), + if (!isVideoControlsVisible) + GestureDetector( + onTap: () { + // Debounce the tap action to account for multiple taps + debounceTimer?.cancel(); + timer?.cancel(); + + debounceTimer = Timer(const Duration(milliseconds: 300), () { + setState(() => isVideoControlsVisible = true); + }); + }, + ), Center( child: AspectRatio( aspectRatio: _videoPlayerController.value.aspectRatio, @@ -152,26 +192,25 @@ class _ThunderVideoPlayerState extends State { setState(() {}); }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOutCubicEmphasized, - color: _videoPlayerController.value.isPlaying ? Colors.transparent : Colors.black.withOpacity(0.2), - ), ), ]), ), ), - VideoPlayerControls( - controller: _videoPlayerController, - onToggleFullScreen: () => setState( - () { - isFullScreen = !isFullScreen; - if (isFullScreen) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); - } else { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - } - }, + AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: isVideoControlsVisible ? 1.0 : 0.0, + child: VideoPlayerControls( + controller: _videoPlayerController, + onToggleFullScreen: () => setState( + () { + isFullScreen = !isFullScreen; + if (isFullScreen) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + } else { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } + }, + ), ), ), ],