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
1 change: 1 addition & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:supportsPictureInPicture="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
Expand Down
1 change: 1 addition & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@

<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
</array>

Expand Down
16 changes: 16 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,22 @@
"@videoScalingFillScreenNotif": {},
"videoScalingFillScreenTitle": "Fill screen",
"@videoScalingFillScreenTitle": {},
"pictureInPictureTitle": "Picture-in-Picture",
"@pictureInPictureTitle": {
"description": "Tooltip on the in-player PiP button"
},
"pictureInPictureAutoTitle": "Auto Picture-in-Picture",
"@pictureInPictureAutoTitle": {
"description": "Title of the auto-enter Picture-in-Picture setting toggle"
},
"pictureInPictureSubtitle": "Automatically enter Picture-in-Picture when you leave the player. The PiP button in the controls always works regardless of this setting.",
"@pictureInPictureSubtitle": {
"description": "Subtitle of the auto-enter Picture-in-Picture setting toggle"
},
"pictureInPictureNotSupported": "Picture-in-Picture is not available on this device",
"@pictureInPictureNotSupported": {
"description": "Snackbar shown when the user taps the PiP button and the OS reports it unsupported"
},
"videoScalingFitHeight": "Fit Height",
"@videoScalingFitHeight": {},
"videoScalingFitWidth": "Fit Width",
Expand Down
1 change: 1 addition & 0 deletions lib/models/settings/video_player_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
@Default(false) bool enableAdvancedVideoOptions,
@Default(true) bool enableEdgeGestures,
@Default(false) bool reverseEdgeGestures,
@Default(true) bool enablePictureInPicture,
}) = _VideoPlayerSettingsModel;

double get volume => internalVolume;
Expand Down
53 changes: 40 additions & 13 deletions lib/models/settings/video_player_settings.freezed.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/models/settings/video_player_settings.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions lib/providers/pip_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'package:fladder/wrappers/pip_manager.dart';

final pipManagerProvider = Provider<PipManager>((ref) {
final manager = PipManager();
ref.onDispose(manager.dispose);
return manager;
});

final pipStateProvider = StreamProvider<bool>((ref) {
final manager = ref.watch(pipManagerProvider);
return manager.isInPip;
});
2 changes: 2 additions & 0 deletions lib/providers/settings/video_player_settings_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,6 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti
void setEnableEdgeGestures(bool value) => state = state.copyWith(enableEdgeGestures: value);

void setReverseEdgeGestures(bool value) => state = state.copyWith(reverseEdgeGestures: value);

void setEnablePictureInPicture(bool value) => state = state.copyWith(enablePictureInPicture: value);
}
12 changes: 12 additions & 0 deletions lib/screens/settings/player_settings_page.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

Expand Down Expand Up @@ -78,6 +80,16 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
),
],
),
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS))
SettingsListTile(
label: Text(context.localized.pictureInPictureAutoTitle),
subLabel: Text(context.localized.pictureInPictureSubtitle),
onTap: () => provider.setEnablePictureInPicture(!videoSettings.enablePictureInPicture),
trailing: Switch(
value: videoSettings.enablePictureInPicture,
onChanged: (value) => provider.setEnablePictureInPicture(value),
),
),
SettingsListTileEnum(
label: Text(context.localized.videoScaling),
current: videoSettings.videoFit.label(context),
Expand Down
28 changes: 27 additions & 1 deletion lib/screens/video_player/video_player.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
Expand All @@ -9,6 +10,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/models/playback/playback_model.dart';
import 'package:fladder/models/playback/tv_playback_model.dart';
import 'package:fladder/providers/pip_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/screens/video_player/components/video_player_guide_wrapper.dart';
Expand All @@ -17,6 +19,7 @@ import 'package:fladder/screens/video_player/video_player_controls.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/themes_data.dart';
import 'package:fladder/widgets/shared/back_intent_dpad.dart';
import 'package:fladder/wrappers/pip_manager.dart';

class VideoPlayer extends ConsumerStatefulWidget {
const VideoPlayer({super.key});
Expand All @@ -33,18 +36,22 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb

late PlaybackModel? currentPlaybackModel = ref.read(playBackModel);

PipManager? _pipManager;

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
//Don't pause on desktop focus loss
if (!(AdaptiveLayout.of(context).isDesktop || kIsWeb)) {
// Don't pause when entering PiP — playback must continue.
final inPip = ref.read(pipStateProvider).asData?.value ?? false;
switch (state) {
case AppLifecycleState.resumed:
if (playing) ref.read(videoPlayerProvider).play();
break;
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
if (playing) ref.read(videoPlayerProvider).pause();
if (playing && !inPip) ref.read(videoPlayerProvider).pause();
break;
default:
break;
Expand All @@ -56,6 +63,7 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
void dispose() {
WidgetsBinding.instance.removeObserver(this);
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
_pipManager?.disable();
super.dispose();
}

Expand All @@ -70,6 +78,16 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
orientations?.isNotEmpty == true ? orientations!.toList() : DeviceOrientation.values);
return ref.read(videoPlayerSettingsProvider.notifier).setSavedBrightness();
});
Future.microtask(_maybeConfigurePip);
}

void _maybeConfigurePip() {
if (kIsWeb) return;
if (!(Platform.isAndroid || Platform.isIOS)) return;
_pipManager = ref.read(pipManagerProvider);
final autoEnter = ref.read(videoPlayerSettingsProvider).enablePictureInPicture;
// 16:9 default — PlayerState does not expose real video dimensions.
_pipManager?.enable(aspectWidth: 16.0, aspectHeight: 9.0, autoEnter: autoEnter);
}

@override
Expand Down Expand Up @@ -103,6 +121,14 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
},
);

ref.listen(
videoPlayerSettingsProvider.select((value) => value.enablePictureInPicture),
(previous, next) {
if (previous == next) return;
_pipManager?.enable(aspectWidth: 16.0, aspectHeight: 9.0, autoEnter: next);
},
);

final player = Padding(
padding: fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right),
child: playerController.videoWidget(
Expand Down
Loading
Loading