Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## [Unreleased]
* 🖥️ [#947](https://github.com/fluttercommunity/chewie/pull/947): Add `ChewieController.disableFullScreenRoute` (default `false`). When `true`, toggling fullscreen no longer pushes/pops Chewie's internal fullscreen route — `isFullScreen` still flips so the control icon updates, but the player stays mounted in place and the host app owns the fullscreen presentation. Thanks [Ortes](https://github.com/Ortes).

## [1.15.0]
* 🌐 [#946](https://github.com/fluttercommunity/chewie/pull/946): 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). Thanks [Ortes](https://github.com/Ortes).
* 🖱️ [#950](https://github.com/fluttercommunity/chewie/pull/950): Show click cursor on hover over Material controls and progress bar. Thanks [Ortes](https://github.com/Ortes).
Expand Down
24 changes: 24 additions & 0 deletions lib/src/chewie_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ class ChewieState extends State<Chewie> {
}

Future<void> listener() async {
if (widget.controller.disableFullScreenRoute) {
// The host app owns the fullscreen presentation; never push/pop a route
// (which on web reparents the <video> element and forces an HLS reload on
// exit). Just mirror the controller's flag so the control icon updates.
_isFullScreen = isControllerFullScreen;
return;
}
if (isControllerFullScreen && !_isFullScreen) {
_wasPlayingBeforeFullScreen =
widget.controller.videoPlayerController.value.isPlaying;
Expand Down Expand Up @@ -354,6 +361,7 @@ class ChewieController extends ChangeNotifier {
this.allowPlaybackSpeedChanging = true,
this.useRootNavigator = true,
this.useNativeFullScreenOnWeb = true,
this.disableFullScreenRoute = false,
this.playbackSpeeds = const [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
this.systemOverlaysOnEnterFullScreen,
this.deviceOrientationsOnEnterFullScreen,
Expand Down Expand Up @@ -409,6 +417,7 @@ class ChewieController extends ChangeNotifier {
bool? allowPlaybackSpeedChanging,
bool? useRootNavigator,
bool? useNativeFullScreenOnWeb,
bool? disableFullScreenRoute,
Duration? hideControlsTimer,
EdgeInsets? controlsSafeAreaMinimum,
List<double>? playbackSpeeds,
Expand Down Expand Up @@ -475,6 +484,8 @@ class ChewieController extends ChangeNotifier {
useRootNavigator: useRootNavigator ?? this.useRootNavigator,
useNativeFullScreenOnWeb:
useNativeFullScreenOnWeb ?? this.useNativeFullScreenOnWeb,
disableFullScreenRoute:
disableFullScreenRoute ?? this.disableFullScreenRoute,
playbackSpeeds: playbackSpeeds ?? this.playbackSpeeds,
systemOverlaysOnEnterFullScreen:
systemOverlaysOnEnterFullScreen ??
Expand Down Expand Up @@ -654,6 +665,19 @@ class ChewieController extends ChangeNotifier {
/// Has no effect on non-web platforms.
final bool useNativeFullScreenOnWeb;

/// When true, toggling fullscreen does NOT push/pop Chewie's own fullscreen
/// route. The controller's [isFullScreen] still flips (so the control icon
/// updates), but the player widget stays mounted in place and the host app is
/// responsible for the fullscreen presentation (e.g. driving the browser
/// Fullscreen API and expanding its own layout).
///
/// On web, Chewie's route-based fullscreen reparents the platform-view
/// `<video>` element; on exit the original view goes blank and the fork works
/// around it by re-initializing the controller — which for an HLS source
/// means a full manifest reload + rebuffer. Setting this avoids that entirely
/// by never moving the element.
final bool disableFullScreenRoute;

/// Defines the [Duration] before the video controls are hidden. By default, this is set to three seconds.
final Duration hideControlsTimer;

Expand Down
88 changes: 88 additions & 0 deletions test/disable_full_screen_route_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import 'package:chewie/chewie.dart';
import 'package:chewie/src/player_with_controls.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:video_player/video_player.dart';

const src =
"https://assets.mixkit.co/videos/preview/mixkit-spinning-around-the-earth-29351-large.mp4";

void main() {
testWidgets(
"disableFullScreenRoute flips isFullScreen without pushing a route",
(WidgetTester tester) async {
final videoPlayerController = VideoPlayerController.networkUrl(
Uri.parse(src),
);
final chewieController = ChewieController(
videoPlayerController: videoPlayerController,
autoPlay: false,
looping: false,
disableFullScreenRoute: true,
);

await tester.pumpWidget(
MaterialApp(
home: Scaffold(body: Chewie(controller: chewieController)),
),
);
await tester.pump();

expect(chewieController.disableFullScreenRoute, isTrue);
expect(chewieController.isFullScreen, isFalse);
// The player is mounted exactly once.
expect(find.byType(PlayerWithControls), findsOneWidget);

// Enter fullscreen: the controller flag flips, but because the route is
// disabled the player stays mounted in place — no second instance is
// pushed onto a fullscreen route.
chewieController.enterFullScreen();
await tester.pumpAndSettle();

expect(chewieController.isFullScreen, isTrue);
expect(find.byType(PlayerWithControls), findsOneWidget);

// Exit fullscreen: the flag flips back, again without a route pop.
chewieController.exitFullScreen();
await tester.pumpAndSettle();

expect(chewieController.isFullScreen, isFalse);
expect(find.byType(PlayerWithControls), findsOneWidget);

chewieController.dispose();
videoPlayerController.dispose();
},
);

test("disableFullScreenRoute defaults to false", () {
final videoPlayerController = VideoPlayerController.networkUrl(
Uri.parse(src),
);
final controller = ChewieController(
videoPlayerController: videoPlayerController,
);

expect(controller.disableFullScreenRoute, isFalse);

controller.dispose();
videoPlayerController.dispose();
});

test("copyWith carries disableFullScreenRoute", () {
final videoPlayerController = VideoPlayerController.networkUrl(
Uri.parse(src),
);
final base = ChewieController(videoPlayerController: videoPlayerController);
expect(base.disableFullScreenRoute, isFalse);

final enabled = base.copyWith(disableFullScreenRoute: true);
expect(enabled.disableFullScreenRoute, isTrue);

// Omitting the argument preserves the existing value.
final preserved = enabled.copyWith();
expect(preserved.disableFullScreenRoute, isTrue);

base.dispose();
videoPlayerController.dispose();
});
}