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

@ -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),