diff --git a/CHANGELOG.md b/CHANGELOG.md index e6a5997aa..f42e2e0c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [Unreleased] +* ✨ Web: enter the browser's native (OS-level) fullscreen via the Fullscreen API instead of only expanding the Flutter view inside the browser window. Pressing Escape to leave browser fullscreen also exits Chewie's fullscreen. Controlled by the new `ChewieController.useNativeFullScreenOnWeb` flag (defaults to `true`; no effect on non-web platforms). + ## [1.14.1] * 🛠️ [#945](https://github.com/fluttercommunity/chewie/pull/945): Flutter 3.38 downgrade. Thanks [diegotori](https://github.com/diegotori). * Library now supports Flutter and Dart versions `3.38.0` and `3.10` or higher respectively, restoring previous compatibility. diff --git a/lib/src/chewie_player.dart b/lib/src/chewie_player.dart index 7ffa295b2..9e02c4265 100644 --- a/lib/src/chewie_player.dart +++ b/lib/src/chewie_player.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:chewie/src/chewie_progress_colors.dart'; +import 'web_fullscreen.dart'; import 'package:chewie/src/models/option_item.dart'; import 'package:chewie/src/models/options_translation.dart'; import 'package:chewie/src/models/subtitle_model.dart'; @@ -44,16 +45,30 @@ class ChewieState extends State { bool get isControllerFullScreen => widget.controller.isFullScreen; late PlayerNotifier notifier; + late final void Function() _browserFsExitHandler; @override void initState() { super.initState(); widget.controller.addListener(listener); notifier = PlayerNotifier.init(); + // When the user presses Escape, the browser exits its native fullscreen + // without Chewie knowing. Detect this and collapse the fullscreen route. + _browserFsExitHandler = () { + if (!browserInFullscreen && _isFullScreen) { + widget.controller.exitFullScreen(); + } + }; + if (widget.controller.useNativeFullScreenOnWeb) { + addBrowserFullscreenChangeListener(_browserFsExitHandler); + } } @override void dispose() { + if (widget.controller.useNativeFullScreenOnWeb) { + removeBrowserFullscreenChangeListener(_browserFsExitHandler); + } widget.controller.removeListener(listener); notifier.dispose(); super.dispose(); @@ -182,6 +197,12 @@ class ChewieState extends State { WakelockPlus.enable(); } + // Ask the browser to enter its native fullscreen. Must be called before the + // first await so we are still inside the user-gesture event handler. + if (widget.controller.useNativeFullScreenOnWeb) { + requestBrowserFullscreen(); + } + await Navigator.of( context, rootNavigator: widget.controller.useRootNavigator, @@ -191,6 +212,11 @@ class ChewieState extends State { if (kIsWeb) { await _reInitializeControllers(wasPlaying); + // Exit native browser fullscreen when the Chewie route pops (e.g. user + // clicked the fullscreen button again). No-op if Escape was already used. + if (widget.controller.useNativeFullScreenOnWeb) { + exitBrowserFullscreen(); + } } _isFullScreen = false; @@ -325,6 +351,7 @@ class ChewieController extends ChangeNotifier { this.allowMuting = true, this.allowPlaybackSpeedChanging = true, this.useRootNavigator = true, + this.useNativeFullScreenOnWeb = true, this.playbackSpeeds = const [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], this.systemOverlaysOnEnterFullScreen, this.deviceOrientationsOnEnterFullScreen, @@ -378,6 +405,7 @@ class ChewieController extends ChangeNotifier { bool? allowMuting, bool? allowPlaybackSpeedChanging, bool? useRootNavigator, + bool? useNativeFullScreenOnWeb, Duration? hideControlsTimer, EdgeInsets? controlsSafeAreaMinimum, List? playbackSpeeds, @@ -441,6 +469,8 @@ class ChewieController extends ChangeNotifier { allowPlaybackSpeedChanging: allowPlaybackSpeedChanging ?? this.allowPlaybackSpeedChanging, useRootNavigator: useRootNavigator ?? this.useRootNavigator, + useNativeFullScreenOnWeb: + useNativeFullScreenOnWeb ?? this.useNativeFullScreenOnWeb, playbackSpeeds: playbackSpeeds ?? this.playbackSpeeds, systemOverlaysOnEnterFullScreen: systemOverlaysOnEnterFullScreen ?? @@ -599,6 +629,14 @@ class ChewieController extends ChangeNotifier { /// Defines if push/pop navigations use the rootNavigator final bool useRootNavigator; + /// On Flutter Web, also enter the browser's native fullscreen (via the + /// Fullscreen API) when going fullscreen, instead of only expanding the + /// Flutter view inside the browser window. Pressing Escape to leave the + /// browser fullscreen also exits Chewie's fullscreen. + /// + /// Has no effect on non-web platforms. + final bool useNativeFullScreenOnWeb; + /// Defines the [Duration] before the video controls are hidden. By default, this is set to three seconds. final Duration hideControlsTimer; diff --git a/lib/src/web_fullscreen.dart b/lib/src/web_fullscreen.dart new file mode 100644 index 000000000..54e6d8961 --- /dev/null +++ b/lib/src/web_fullscreen.dart @@ -0,0 +1,2 @@ +export 'web_fullscreen_stub.dart' + if (dart.library.js_interop) 'web_fullscreen_impl.dart'; diff --git a/lib/src/web_fullscreen_impl.dart b/lib/src/web_fullscreen_impl.dart new file mode 100644 index 000000000..86dddc778 --- /dev/null +++ b/lib/src/web_fullscreen_impl.dart @@ -0,0 +1,30 @@ +import 'dart:js_interop'; + +import 'package:web/web.dart' as web; + +final _handlers = {}; + +void requestBrowserFullscreen() { + web.document.documentElement?.requestFullscreen(); +} + +void exitBrowserFullscreen() { + if (web.document.fullscreenElement != null) { + web.document.exitFullscreen(); + } +} + +bool get browserInFullscreen => web.document.fullscreenElement != null; + +void addBrowserFullscreenChangeListener(void Function() callback) { + final jsHandler = ((JSAny? _) => callback()).toJS; + _handlers[callback] = jsHandler; + web.document.addEventListener('fullscreenchange', jsHandler); +} + +void removeBrowserFullscreenChangeListener(void Function() callback) { + final jsHandler = _handlers.remove(callback); + if (jsHandler != null) { + web.document.removeEventListener('fullscreenchange', jsHandler); + } +} diff --git a/lib/src/web_fullscreen_stub.dart b/lib/src/web_fullscreen_stub.dart new file mode 100644 index 000000000..47583a874 --- /dev/null +++ b/lib/src/web_fullscreen_stub.dart @@ -0,0 +1,5 @@ +void requestBrowserFullscreen() {} +void exitBrowserFullscreen() {} +bool get browserInFullscreen => false; +void addBrowserFullscreenChangeListener(void Function() callback) {} +void removeBrowserFullscreenChangeListener(void Function() callback) {} diff --git a/pubspec.yaml b/pubspec.yaml index 0bd6b9ad5..b38e1b6a3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: provider: ^6.1.5+1 video_player: ^2.11.1 wakelock_plus: ^1.6.1 + web: ^1.0.0 dev_dependencies: flutter_test: