feature: Added LibMDK video player backend (#162)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2024-11-22 18:53:31 +01:00 committed by GitHub
parent 6e32018183
commit da354437e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 1499 additions and 1006 deletions

View file

@ -37,7 +37,7 @@ class MediaStreamInformation extends ConsumerWidget {
_StreamOptionSelect(
label: Text(context.localized.audio),
current: mediaStream.currentAudioStream?.displayTitle ?? "",
itemBuilder: (context) => mediaStream.audioStreams
itemBuilder: (context) => [AudioStreamModel.no(), ...mediaStream.audioStreams]
.map(
(e) => PopupMenuItem(
value: e,

View file

@ -8,7 +8,6 @@ import 'package:fladder/models/account_model.dart';
import 'package:fladder/providers/auth_provider.dart';
import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/shared/user_icon.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/list_padding.dart';
class LoginUserGrid extends ConsumerWidget {
@ -37,79 +36,87 @@ class LoginUserGrid extends ConsumerWidget {
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return _CardHolder(
return FlatButton(
key: Key(user.id),
content: Stack(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: UserIcon(
labelStyle: Theme.of(context).textTheme.headlineMedium,
user: user,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
onTap: () => editMode ? onLongPress?.call(user) : onPressed?.call(user),
onLongPress: () => onLongPress?.call(user),
child: _CardHolder(
content: Stack(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
user.authMethod.icon,
size: 18,
),
const SizedBox(width: 4),
Flexible(
child: Text(
user.name,
maxLines: 2,
softWrap: true,
)),
],
),
if (user.credentials.serverName.isNotEmpty)
Opacity(
opacity: 0.75,
child: Row(
child: UserIcon(
labelStyle: Theme.of(context).textTheme.headlineMedium,
user: user,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
const Icon(
IconsaxBold.driver_2,
size: 14,
Icon(
user.authMethod.icon,
size: 18,
),
const SizedBox(width: 4),
Flexible(
child: Text(
user.credentials.serverName,
maxLines: 2,
softWrap: true,
),
),
child: Text(
user.name,
maxLines: 2,
softWrap: true,
)),
],
),
)
].addInBetween(const SizedBox(width: 4, height: 4)),
),
if (editMode)
Align(
alignment: Alignment.topRight,
child: Card(
color: Theme.of(context).colorScheme.errorContainer,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
IconsaxBold.edit_2,
size: 14,
if (user.credentials.serverName.isNotEmpty)
Opacity(
opacity: 0.75,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
const Icon(
IconsaxBold.driver_2,
size: 14,
),
const SizedBox(width: 4),
Flexible(
child: Text(
user.credentials.serverName,
maxLines: 2,
softWrap: true,
),
),
],
),
)
].addInBetween(const SizedBox(width: 4, height: 4)),
),
),
if (editMode)
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
color: Theme.of(context).colorScheme.errorContainer,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
IconsaxBold.edit_2,
size: 14,
),
),
),
),
),
)
],
],
),
),
onTap: () => editMode ? onLongPress?.call(user) : onPressed?.call(user),
onLongPress: () => onLongPress?.call(user),
);
},
);
@ -118,13 +125,9 @@ class LoginUserGrid extends ConsumerWidget {
class _CardHolder extends StatelessWidget {
final Widget content;
final Function() onTap;
final Function() onLongPress;
const _CardHolder({
required this.content,
required this.onTap,
required this.onLongPress,
super.key,
});
@ -137,14 +140,7 @@ class _CardHolder extends StatelessWidget {
margin: EdgeInsets.zero,
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 150, maxWidth: 150),
child: FlatButton(
onTap: onTap,
onLongPress: AdaptiveLayout.of(context).isDesktop ? onLongPress : null,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: content,
),
),
child: content,
),
);
}

View file

@ -1,20 +1,24 @@
import 'dart:async';
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/providers/settings/photo_view_settings_provider.dart';
import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/widgets/shared/fladder_slider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/util/duration_extensions.dart';
import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:window_manager/window_manager.dart';
import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/models/settings/video_player_settings.dart';
import 'package:fladder/providers/settings/photo_view_settings_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/util/duration_extensions.dart';
import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/widgets/shared/fladder_slider.dart';
import 'package:fladder/wrappers/players/lib_mdk.dart'
if (dart.library.html) 'package:fladder/stubs/web/lib_mdk_web.dart';
import 'package:fladder/wrappers/players/lib_mpv.dart';
class SimpleVideoPlayer extends ConsumerStatefulWidget {
final PhotoModel video;
final bool showOverlay;
@ -26,10 +30,10 @@ class SimpleVideoPlayer extends ConsumerStatefulWidget {
}
class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with WindowListener, WidgetsBindingObserver {
final Player player = Player(
configuration: const PlayerConfiguration(title: "nl.jknaapen.fladder", libass: true),
);
late VideoController controller = VideoController(player);
late final player = switch (ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer))) {
PlayerOptions.libMDK => LibMDK(),
PlayerOptions.libMPV => LibMPV(),
};
late String videoUrl = "";
bool playing = false;
@ -61,9 +65,9 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
super.initState();
windowManager.addListener(this);
WidgetsBinding.instance.addObserver(this);
playing = player.state.playing;
position = player.state.position;
duration = player.state.duration;
playing = player.lastState.playing;
position = player.lastState.position;
duration = player.lastState.duration;
Future.microtask(() async => {_init()});
}
@ -84,43 +88,21 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
videoUrl = '${ref.read(userProvider)?.server ?? ""}/Videos/${widget.video.id}/stream?$params';
subscriptions.addAll(
[
player.stream.playing.listen((event) {
setState(() {
playing = event;
});
if (playing) {
WakelockPlus.enable();
} else {
WakelockPlus.disable();
}
}),
player.stream.position.listen((event) {
setState(() {
position = event;
});
}),
player.stream.completed.listen((event) {
if (event) {
_restartVideo();
}
}),
player.stream.duration.listen((event) {
setState(() {
duration = event;
});
}),
],
);
subscriptions.add(player.stateStream.listen((event) {
setState(() {
playing = event.playing;
position = event.position;
duration = event.duration;
});
if (playing) {
WakelockPlus.enable();
} else {
WakelockPlus.disable();
}
}));
await player.open(videoUrl, !ref.watch(photoViewSettingsProvider).autoPlay);
await player.loop(ref.watch(photoViewSettingsProvider.select((value) => value.repeat)));
await player.setVolume(ref.watch(photoViewSettingsProvider.select((value) => value.mute)) ? 0 : 100);
await player.open(Media(videoUrl), play: !ref.watch(photoViewSettingsProvider).autoPlay);
}
void _restartVideo() {
if (ref.read(photoViewSettingsProvider.select((value) => value.repeat))) {
player.play();
}
}
@override
@ -142,6 +124,9 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.bold, shadows: [const Shadow(blurRadius: 2)]);
ref.listen(photoViewSettingsProvider.select((value) => value.repeat), (previous, next) {
player.loop(next);
});
ref.listen(
photoViewSettingsProvider.select((value) => value.mute),
(previous, next) {
@ -165,13 +150,17 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
//Fixes small overlay problems with thumbnail
Transform.scale(
scaleY: 1.004,
child: Video(
fit: BoxFit.contain,
fill: const Color.fromARGB(0, 123, 62, 62),
controller: controller,
controls: NoVideoControls,
wakelock: false,
child: player.videoWidget(
UniqueKey(),
BoxFit.contain,
),
// child: Video(
// fit: BoxFit.contain,
// fill: const Color.fromARGB(0, 123, 62, 62),
// controller: controller,
// controls: NoVideoControls,
// wakelock: false,
// ),
),
IgnorePointer(
ignoring: !widget.showOverlay,
@ -211,7 +200,7 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
}
},
onChangeStart: (value) {
wasPlaying = player.state.playing;
wasPlaying = player.lastState.playing;
player.pause();
},
onChanged: (e) {
@ -239,7 +228,7 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
player.playOrPause();
},
icon: Icon(
player.state.playing ? IconsaxBold.pause_circle : IconsaxBold.play_circle,
player.lastState.playing ? IconsaxBold.pause_circle : IconsaxBold.play_circle,
shadows: [
BoxShadow(blurRadius: 16, spreadRadius: 2, color: Colors.black.withOpacity(0.15))
],

View file

@ -82,34 +82,90 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
),
const Divider(),
SettingsLabelDivider(label: context.localized.advanced),
SettingsListTile(
label: Text(context.localized.settingsPlayerVideoHWAccelTitle),
subLabel: Text(context.localized.settingsPlayerVideoHWAccelDesc),
onTap: () => provider.setHardwareAccel(!videoSettings.hardwareAccel),
trailing: Switch(
value: videoSettings.hardwareAccel,
onChanged: (value) => provider.setHardwareAccel(value),
),
),
if (!kIsWeb) ...[
if (PlayerOptions.available.length != 1)
SettingsListTile(
label: Text(context.localized.settingsPlayerNativeLibassAccelTitle),
subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc),
onTap: () => provider.setUseLibass(!videoSettings.useLibass),
trailing: Switch(
value: videoSettings.useLibass,
onChanged: (value) => provider.setUseLibass(value),
),
),
AnimatedFadeSize(
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
? SettingsMessageBox(
context.localized.settingsPlayerMobileWarning,
messageType: MessageType.warning,
label: Text(context.localized.playerSettingsBackendTitle),
subLabel: Text(context.localized.playerSettingsBackendDesc),
trailing: Builder(builder: (context) {
final wantedPlayer = ref.watch(videoPlayerSettingsProvider.select((value) => value.wantedPlayer));
final currentPlayer = ref.watch(videoPlayerSettingsProvider.select((value) => value.playerOptions));
return EnumBox(
current: currentPlayer == null
? "${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"
: wantedPlayer.label(context),
itemBuilder: (context) => [
PopupMenuItem(
value: null,
child:
Text("${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
ref.read(videoPlayerSettingsProvider).copyWith(playerOptions: null),
),
...PlayerOptions.available.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
ref.read(videoPlayerSettingsProvider).copyWith(playerOptions: entry),
),
)
: Container(),
],
);
}),
),
],
AnimatedFadeSize(
child: switch (ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer))) {
PlayerOptions.libMPV => Column(
children: [
SettingsListTile(
label: Text(context.localized.settingsPlayerVideoHWAccelTitle),
subLabel: Text(context.localized.settingsPlayerVideoHWAccelDesc),
onTap: () => provider.setHardwareAccel(!videoSettings.hardwareAccel),
trailing: Switch(
value: videoSettings.hardwareAccel,
onChanged: (value) => provider.setHardwareAccel(value),
),
),
if (!kIsWeb) ...[
SettingsListTile(
label: Text(context.localized.settingsPlayerNativeLibassAccelTitle),
subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc),
onTap: () => provider.setUseLibass(!videoSettings.useLibass),
trailing: Switch(
value: videoSettings.useLibass,
onChanged: (value) => provider.setUseLibass(value),
),
),
AnimatedFadeSize(
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
? SettingsMessageBox(
context.localized.settingsPlayerMobileWarning,
messageType: MessageType.warning,
)
: Container(),
),
],
SettingsListTile(
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
onTap: videoSettings.useLibass
? null
: () {
showDialog(
context: context,
barrierDismissible: false,
useSafeArea: false,
builder: (context) => const SubtitleEditor(),
);
},
),
],
),
_ => SettingsMessageBox(
messageType: MessageType.info,
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}")
},
),
SettingsListTile(
label: Text(context.localized.settingsAutoNextTitle),
subLabel: Text(context.localized.settingsAutoNextDesc),
@ -138,20 +194,6 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
_ => const SizedBox.shrink(),
},
),
SettingsListTile(
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
onTap: videoSettings.useLibass
? null
: () {
showDialog(
context: context,
barrierDismissible: false,
useSafeArea: false,
builder: (context) => const SubtitleEditor(),
);
},
),
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
SettingsListTile(
label: Text(context.localized.playerSettingsOrientationTitle),

View file

@ -1,3 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/settings/subtitle_settings_model.dart';
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
@ -5,10 +10,6 @@ import 'package:fladder/screens/video_player/components/video_subtitle_controls.
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/navigation_scaffold/components/fladder_appbar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ignore: depend_on_referenced_packages
class SubtitleEditor extends ConsumerStatefulWidget {
const SubtitleEditor({super.key});

View file

@ -14,35 +14,43 @@ class FlatButton extends ConsumerWidget {
final Color? splashColor;
final double elevation;
final Clip clipBehavior;
const FlatButton(
{this.child,
this.onTap,
this.onLongPress,
this.onDoubleTap,
this.onSecondaryTapDown,
this.borderRadiusGeometry,
this.splashColor,
this.elevation = 0,
this.clipBehavior = Clip.none,
super.key});
const FlatButton({
this.child,
this.onTap,
this.onLongPress,
this.onDoubleTap,
this.onSecondaryTapDown,
this.borderRadiusGeometry,
this.splashColor,
this.elevation = 0,
this.clipBehavior = Clip.none,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Material(
color: Colors.transparent,
clipBehavior: clipBehavior,
borderRadius: borderRadiusGeometry ?? FladderTheme.defaultShape.borderRadius,
elevation: 0,
child: InkWell(
onTap: onTap,
onLongPress: onLongPress,
onDoubleTap: onDoubleTap,
onSecondaryTapDown: onSecondaryTapDown,
borderRadius: borderRadiusGeometry ?? BorderRadius.circular(10),
splashColor: splashColor ?? Theme.of(context).colorScheme.primary.withOpacity(0.5),
splashFactory: InkSparkle.splashFactory,
child: child ?? Container(),
),
return Stack(
fit: StackFit.passthrough,
children: [
child ?? Container(),
Positioned.fill(
child: Material(
color: Colors.transparent,
clipBehavior: clipBehavior,
borderRadius: borderRadiusGeometry ?? FladderTheme.defaultShape.borderRadius,
elevation: 0,
child: InkWell(
onTap: onTap,
onLongPress: onLongPress,
onDoubleTap: onDoubleTap,
onSecondaryTapDown: onSecondaryTapDown,
borderRadius: borderRadiusGeometry ?? BorderRadius.circular(10),
splashColor: splashColor ?? Theme.of(context).colorScheme.primary.withOpacity(0.5),
splashFactory: InkSparkle.splashFactory,
),
),
),
],
);
}
}

View file

@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/playback/playback_model.dart';
import 'package:fladder/providers/session_info_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/util/localization_helper.dart';
Future<void> showVideoPlaybackInformation(BuildContext context) {
return showDialog(
@ -19,6 +22,7 @@ class _VideoPlaybackInformation extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final playbackModel = ref.watch(playBackModel);
final sessionInfo = ref.watch(sessionInfoProvider);
final backend = ref.read(videoPlayerProvider.select((value) => value.backend));
return Dialog(
child: Padding(
padding: const EdgeInsets.all(12.0),
@ -27,47 +31,81 @@ class _VideoPlaybackInformation extends ConsumerWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Playback information", style: Theme.of(context).textTheme.titleMedium),
Text("Player info", style: Theme.of(context).textTheme.titleMedium),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4),
child: Opacity(
opacity: 0.80,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [const Text('backend: '), Text(backend?.label(context) ?? context.localized.unknown)],
)
].addPadding(const EdgeInsets.symmetric(vertical: 3)),
),
),
),
const Divider(),
...[
Row(
mainAxisSize: MainAxisSize.min,
children: [const Text('type: '), Text(playbackModel.label ?? "")],
),
if (sessionInfo.transCodeInfo != null) ...[
const SizedBox(height: 6),
Text("Transcoding", style: Theme.of(context).textTheme.titleMedium),
if (sessionInfo.transCodeInfo?.transcodeReasons?.isNotEmpty == true)
Row(
mainAxisSize: MainAxisSize.min,
children: [const Text('reason: '), Text(sessionInfo.transCodeInfo?.transcodeReasons.toString() ?? "")],
),
if (sessionInfo.transCodeInfo?.completionPercentage != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('transcode progress: '),
Text("${sessionInfo.transCodeInfo?.completionPercentage?.toStringAsFixed(2)} %")
Text("Playback information", style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4),
child: Opacity(
opacity: 0.8,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [const Text('type: '), Text(playbackModel.label ?? "")],
),
if (sessionInfo.transCodeInfo != null) ...[
Text("Transcoding", style: Theme.of(context).textTheme.titleMedium),
if (sessionInfo.transCodeInfo?.transcodeReasons?.isNotEmpty == true)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('reason: '),
Text(sessionInfo.transCodeInfo?.transcodeReasons.toString() ?? "")
],
),
if (sessionInfo.transCodeInfo?.completionPercentage != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('transcode progress: '),
Text("${sessionInfo.transCodeInfo?.completionPercentage?.toStringAsFixed(2)} %")
],
),
if (sessionInfo.transCodeInfo?.container != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('container: '),
Text(sessionInfo.transCodeInfo!.container.toString())
],
),
],
),
if (sessionInfo.transCodeInfo?.container != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [const Text('container: '), Text(sessionInfo.transCodeInfo!.container.toString())],
),
],
Row(
mainAxisSize: MainAxisSize.min,
children: [const Text('resolution: '), Text(playbackModel?.item.streamModel?.resolutionText ?? "")],
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('resolution: '),
Text(playbackModel?.item.streamModel?.resolutionText ?? "")
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('container: '),
Text(playbackModel?.playbackInfo?.mediaSources?.firstOrNull?.container ?? "")
],
)
].addPadding(const EdgeInsets.symmetric(vertical: 3)),
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('container: '),
Text(playbackModel?.playbackInfo?.mediaSources?.firstOrNull?.container ?? "")
],
),
].addPadding(const EdgeInsets.symmetric(vertical: 3))
),
],
),
),

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/screens/video_player/components/video_player_chapters.dart';
@ -9,8 +8,7 @@ import 'package:fladder/screens/video_player/components/video_player_queue.dart'
class ChapterButton extends ConsumerWidget {
final Duration position;
final Player player;
const ChapterButton({super.key, required this.position, required this.player});
const ChapterButton({super.key, required this.position});
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -22,9 +20,9 @@ class ChapterButton extends ConsumerWidget {
context,
chapters: currentChapters,
currentPosition: position,
onChapterTapped: (chapter) => player.seek(
chapter.startPosition,
),
onChapterTapped: (chapter) => ref.read(videoPlayerProvider).seek(
chapter.startPosition,
),
);
},
icon: const Icon(

View file

@ -447,13 +447,13 @@ class _SimpleControls extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(videoPlayerProvider.select((value) => value.controller?.player));
final player = ref.watch(videoPlayerProvider);
final isPlaying = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
return Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton.filledTonal(
onPressed: () => player?.playOrPause(),
onPressed: () => player.playOrPause(),
icon: Icon(isPlaying ? IconsaxBold.pause : IconsaxBold.play),
),
if (skip != null)

View file

@ -12,6 +12,7 @@ import 'package:fladder/models/playback/direct_playback_model.dart';
import 'package:fladder/models/playback/offline_playback_model.dart';
import 'package:fladder/models/playback/playback_model.dart';
import 'package:fladder/models/playback/transcode_playback_model.dart';
import 'package:fladder/models/settings/video_player_settings.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
@ -375,15 +376,16 @@ Future<void> showSubSelection(BuildContext context) {
children: [
Text(context.localized.subtitle),
const Spacer(),
IconButton.outlined(
onPressed: () {
Navigator.pop(context);
showSubtitleControls(
context: context,
label: context.localized.subtitleConfiguration,
);
},
icon: const Icon(Icons.display_settings_rounded))
if (player.backend == PlayerOptions.libMPV)
IconButton.outlined(
onPressed: () {
Navigator.pop(context);
showSubtitleControls(
context: context,
label: context.localized.subtitleConfiguration,
);
},
icon: const Icon(Icons.display_settings_rounded))
],
),
children: playbackModel?.subStreams?.mapIndexed(
@ -459,7 +461,7 @@ Future<void> showPlaybackSpeed(BuildContext context) {
return StatefulBuilder(builder: (context, setState) {
return Consumer(
builder: (context, ref, child) {
final player = ref.watch(videoPlayerProvider.select((value) => value.player));
final player = ref.watch(videoPlayerProvider);
final lastSpeed = ref.watch(playbackRateProvider);
return SimpleDialog(
contentPadding: const EdgeInsets.only(top: 8, bottom: 24),
@ -484,7 +486,7 @@ Future<void> showPlaybackSpeed(BuildContext context) {
divisions: 39,
onChanged: (value) {
ref.read(playbackRateProvider.notifier).state = value;
player?.setRate(value);
player.setSpeed(value);
},
),
),

View file

@ -122,7 +122,7 @@ class _ChapterProgressSliderState extends ConsumerState<VideoProgressBar> {
setState(() {
onHoverStart = true;
});
widget.wasPlayingChanged.call(player.player?.state.playing ?? false);
widget.wasPlayingChanged.call(player.lastState?.playing ?? false);
player.pause();
},
onChanged: (e) {

View file

@ -1,64 +0,0 @@
import 'dart:async';
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:fladder/models/settings/subtitle_settings_model.dart';
class VideoSubtitles extends ConsumerStatefulWidget {
final VideoController controller;
final bool overLayed;
const VideoSubtitles({
required this.controller,
this.overLayed = false,
super.key,
});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _VideoSubtitlesState();
}
class _VideoSubtitlesState extends ConsumerState<VideoSubtitles> {
late List<String> subtitle = widget.controller.player.state.subtitle;
StreamSubscription<List<String>>? subscription;
@override
void initState() {
subscription = widget.controller.player.stream.subtitle.listen((value) {
setState(() {
subtitle = value;
});
});
super.initState();
}
@override
void dispose() {
subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final settings = ref.watch(subtitleSettingsProvider);
final padding = MediaQuery.of(context).padding;
final text = [
for (final line in subtitle)
if (line.trim().isNotEmpty) line.trim(),
].join('\n');
if (widget.controller.player.platform?.configuration.libass ?? false) {
return const IgnorePointer(child: SizedBox());
} else {
return SubtitleText(
subModel: settings,
padding: padding,
offset: (widget.overLayed ? 0.5 : settings.verticalOffset),
text: text,
);
}
}
}

View file

@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
@ -73,7 +72,7 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
final videoFit = ref.watch(videoPlayerSettingsProvider.select((value) => value.videoFit));
final padding = MediaQuery.of(context).padding;
final playerController = ref.watch(videoPlayerProvider.select((value) => value.controller));
final playerController = ref.watch(videoPlayerProvider.select((value) => value));
ref.listen(
videoPlayerSettingsProvider.select((value) => value.allowedOrientations),
@ -103,24 +102,15 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
lastScale = 0.0;
},
child: VideoPlayerNextWrapper(
video: playerController != null
? Padding(
padding: fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right),
child: OrientationBuilder(builder: (context, orientation) {
return Video(
key: Key(orientation.toString()),
controller: playerController,
fill: Colors.transparent,
wakelock: true,
fit: fillScreen
? (MediaQuery.of(context).orientation == Orientation.portrait ? videoFit : BoxFit.cover)
: videoFit,
subtitleViewConfiguration: const SubtitleViewConfiguration(visible: false),
controls: NoVideoControls,
);
}),
)
: const SizedBox.shrink(),
video: Padding(
padding: fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right),
child: playerController.videoWidget(
const Key("VideoPlayer"),
fillScreen
? (MediaQuery.of(context).orientation == Orientation.portrait ? videoFit : BoxFit.cover)
: videoFit,
),
),
controls: const DesktopControls(),
overlays: [
if (errorPlaying) const _VideoErrorWidget(),

View file

@ -1,6 +1,5 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -23,7 +22,6 @@ import 'package:fladder/screens/video_player/components/video_player_controls_ex
import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart';
import 'package:fladder/screens/video_player/components/video_player_seek_indicator.dart';
import 'package:fladder/screens/video_player/components/video_progress_bar.dart';
import 'package:fladder/screens/video_player/components/video_subtitles.dart';
import 'package:fladder/screens/video_player/components/video_volume_slider.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/duration_extensions.dart';
@ -31,8 +29,7 @@ import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/string_extensions.dart';
import 'package:fladder/widgets/shared/full_screen_button.dart'
if (dart.library.html) 'package:fladder/widgets/shared/full_screen_button_web.dart';
import 'package:fladder/widgets/shared/full_screen_button.dart';
class DesktopControls extends ConsumerStatefulWidget {
const DesktopControls({super.key});
@ -115,7 +112,8 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
@override
Widget build(BuildContext context) {
final mediaSegments = ref.watch(playBackModel.select((value) => value?.mediaSegments));
final player = ref.watch(videoPlayerProvider.select((value) => value.controller));
final player = ref.watch(videoPlayerProvider);
final subtitleWidget = player.subtitleWidget(showOverlay);
return InputHandler(
autoFocus: false,
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
@ -135,12 +133,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
onHover: AdaptiveLayout.of(context).isDesktop ? (event) => toggleOverlay(value: true) : null,
child: Stack(
children: [
if (player != null)
VideoSubtitles(
key: const Key('subtitles'),
controller: player,
overLayed: showOverlay,
),
if (subtitleWidget != null) subtitleWidget,
if (AdaptiveLayout.of(context).isDesktop)
Consumer(builder: (context, ref, child) {
final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
@ -385,7 +378,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
onPressed: () => closePlayer(), icon: const Icon(IconsaxOutline.close_square))),
const Spacer(),
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer &&
ref.read(videoPlayerProvider).player != null) ...{
ref.read(videoPlayerProvider).hasPlayer) ...{
// OpenQueueButton(x),
// ChapterButton(
// position: position,
@ -471,7 +464,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
),
),
),
}
},
].addPadding(const EdgeInsets.symmetric(horizontal: 4)),
),
const SizedBox(height: 4),