diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15a17ad..59bb0b6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,13 @@ on: - "v*" branches: - master + pull_request: + paths: + - pubspec.yaml + - .github/workflows/build.yml + types: + - opened + - reopened workflow_dispatch: jobs: @@ -228,6 +235,7 @@ jobs: build-linux-flatpak: name: "Flatpak" runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') needs: [fetch-info, build-linux] container: image: bilelmoussaoui/flatpak-github-actions:gnome-46 diff --git a/assets/fonts/mp-font.ttf b/assets/mp-font.ttf similarity index 100% rename from assets/fonts/mp-font.ttf rename to assets/mp-font.ttf diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9bd0b03..7c58111 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1125,5 +1125,13 @@ "stop": "Stop", "resumeVideo": "Resume video", "closeVideo": "Close video", - "playNextVideo": "Play next video" + "playNextVideo": "Play next video", + "playerSettingsBackendTitle": "Video player Backend", + "playerSettingsBackendDesc": "Choose your preferred media player for optimal playback experience", + "defaultLabel": "Default", + "@defaultLabel": { + "description": "To indicate a default value, default video player backend" + }, + "noVideoPlayerOptions": "The selected backend has no options", + "mdkExperimental": "MDK is still in a experimental stage" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 90fb94e..e09fc93 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,7 +13,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; -import 'package:media_kit/media_kit.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; @@ -67,7 +66,6 @@ Future> loadConfig() async { void main() async { _setupLogging(); WidgetsFlutterBinding.ensureInitialized(); - MediaKit.ensureInitialized(); if (kIsWeb) { html.document.onContextMenu.listen((event) => event.preventDefault()); diff --git a/lib/models/items/trick_play_model.dart b/lib/models/items/trick_play_model.dart index 1aaeddc..0015f94 100644 --- a/lib/models/items/trick_play_model.dart +++ b/lib/models/items/trick_play_model.dart @@ -22,8 +22,8 @@ class TrickPlayModel with _$TrickPlayModel { int get imagesPerTile => tileWidth * tileHeight; String? getTile(Duration position) { - final int currentIndex = (position.inMilliseconds ~/ interval.inMilliseconds).clamp(0, thumbnailCount - 1); - final int indexOfTile = (currentIndex ~/ imagesPerTile).clamp(0, (images.length - 1)); + final int currentIndex = (position.inMilliseconds ~/ interval.inMilliseconds).clamp(0, thumbnailCount); + final int indexOfTile = (currentIndex ~/ imagesPerTile).clamp(0, images.length); return images.elementAtOrNull(indexOfTile); } diff --git a/lib/models/playback/direct_playback_model.dart b/lib/models/playback/direct_playback_model.dart index 2eec700..a57b278 100644 --- a/lib/models/playback/direct_playback_model.dart +++ b/lib/models/playback/direct_playback_model.dart @@ -1,6 +1,7 @@ +import 'package:flutter/widgets.dart'; + import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:media_kit/media_kit.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -13,9 +14,7 @@ import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/util/duration_extensions.dart'; import 'package:fladder/util/list_extensions.dart'; -import 'package:fladder/wrappers/media_control_wrapper.dart' - if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart'; -import 'package:flutter/widgets.dart'; +import 'package:fladder/wrappers/media_control_wrapper.dart'; class DirectPlaybackModel implements PlaybackModel { DirectPlaybackModel({ @@ -67,22 +66,8 @@ class DirectPlaybackModel implements PlaybackModel { @override Future setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async { - final wantedSubtitle = - model ?? subStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultSubStreamIndex); - if (wantedSubtitle == null) return this; - if (wantedSubtitle.index == SubStreamModel.no().index) { - await player.setSubtitleTrack(SubtitleTrack.no()); - } else { - final subTracks = player.subTracks.getRange(2, player.subTracks.length).toList(); - final index = subStreams.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id); - final subTrack = subTracks.elementAtOrNull(index); - if (wantedSubtitle.isExternal && wantedSubtitle.url != null && subTrack == null) { - await player.setSubtitleTrack(SubtitleTrack.uri(wantedSubtitle.url!)); - } else if (subTrack != null) { - await player.setSubtitleTrack(subTrack); - } - } - return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: wantedSubtitle.index)); + final newIndex = await player.setSubtitleTrack(model, this); + return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: newIndex)); } @override @@ -90,19 +75,8 @@ class DirectPlaybackModel implements PlaybackModel { @override Future? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async { - final wantedAudioStream = - model ?? audioStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultAudioStreamIndex); - if (wantedAudioStream == null) return this; - if (wantedAudioStream.index == AudioStreamModel.no().index) { - await player.setAudioTrack(AudioTrack.no()); - } else { - final audioTracks = player.audioTracks.getRange(2, player.audioTracks.length).toList(); - final audioTrack = audioTracks.elementAtOrNull(audioStreams.indexOf(wantedAudioStream) - 1); - if (audioTrack != null) { - await player.setAudioTrack(audioTrack); - } - } - return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: wantedAudioStream.index)); + final newIndex = await player.setAudioTrack(model, this); + return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: newIndex)); } @override diff --git a/lib/models/playback/offline_playback_model.dart b/lib/models/playback/offline_playback_model.dart index ef4e209..71e07cb 100644 --- a/lib/models/playback/offline_playback_model.dart +++ b/lib/models/playback/offline_playback_model.dart @@ -1,6 +1,6 @@ -import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:media_kit/media_kit.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -13,9 +13,7 @@ import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/util/duration_extensions.dart'; import 'package:fladder/util/list_extensions.dart'; -import 'package:fladder/wrappers/media_control_wrapper.dart' - if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart'; -import 'package:flutter/widgets.dart'; +import 'package:fladder/wrappers/media_control_wrapper.dart'; class OfflinePlaybackModel implements PlaybackModel { OfflinePlaybackModel({ @@ -66,22 +64,8 @@ class OfflinePlaybackModel implements PlaybackModel { @override Future setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async { - final wantedSubtitle = - model ?? subStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultSubStreamIndex); - if (wantedSubtitle == null) return this; - if (wantedSubtitle.index == SubStreamModel.no().index) { - await player.setSubtitleTrack(SubtitleTrack.no()); - } else { - final subTracks = player.subTracks.getRange(2, player.subTracks.length).toList(); - final index = subStreams.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id); - final subTrack = subTracks.elementAtOrNull(index); - if (wantedSubtitle.isExternal && wantedSubtitle.url != null && subTrack == null) { - await player.setSubtitleTrack(SubtitleTrack.uri(wantedSubtitle.url!)); - } else if (subTrack != null) { - await player.setSubtitleTrack(subTrack); - } - } - return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: wantedSubtitle.index)); + final newIndex = await player.setSubtitleTrack(model, this); + return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: newIndex)); } @override @@ -89,19 +73,8 @@ class OfflinePlaybackModel implements PlaybackModel { @override Future? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async { - final wantedAudioStream = - model ?? audioStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultAudioStreamIndex); - if (wantedAudioStream == null) return this; - if (wantedAudioStream.index == AudioStreamModel.no().index) { - await player.setAudioTrack(AudioTrack.no()); - } else { - final audioTracks = player.audioTracks.getRange(2, player.audioTracks.length).toList(); - final audioTrack = audioTracks.elementAtOrNull(audioStreams.indexOf(wantedAudioStream) - 1); - if (audioTrack != null) { - await player.setAudioTrack(audioTrack); - } - } - return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: wantedAudioStream.index)); + final newIndex = await player.setAudioTrack(model, this); + return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: newIndex)); } @override diff --git a/lib/models/playback/playback_model.dart b/lib/models/playback/playback_model.dart index 2e2a018..6a38a49 100644 --- a/lib/models/playback/playback_model.dart +++ b/lib/models/playback/playback_model.dart @@ -3,7 +3,6 @@ import 'dart:developer'; import 'package:chopper/chopper.dart'; import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:media_kit/media_kit.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -27,10 +26,23 @@ import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/util/duration_extensions.dart'; -import 'package:fladder/wrappers/media_control_wrapper.dart' - if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart'; +import 'package:fladder/wrappers/media_control_wrapper.dart'; + +class Media { + final String url; + + const Media({ + required this.url, + }); +} extension PlaybackModelExtension on PlaybackModel? { + SubStreamModel? get defaultSubStream => + this?.subStreams?.firstWhereOrNull((element) => element.index == this?.mediaStreams?.defaultSubStreamIndex); + + AudioStreamModel? get defaultAudioStream => + this?.audioStreams?.firstWhereOrNull((element) => element.index == this?.mediaStreams?.defaultAudioStreamIndex); + String? get label => switch (this) { DirectPlaybackModel _ => PlaybackType.directStream.name, TranscodePlaybackModel _ => PlaybackType.transcode.name, @@ -119,7 +131,7 @@ class PlaybackModelHelper { syncedItem: syncedItem, trickPlay: syncedItem.trickPlayModel, mediaSegments: syncedItem.mediaSegments, - media: Media(syncedItem.videoFile.path), + media: Media(url: syncedItem.videoFile.path), queue: itemQueue.whereNotNull().toList(), syncedQueue: children, mediaStreams: item.streamModel ?? syncedItemModel.streamModel, @@ -170,7 +182,7 @@ class PlaybackModelHelper { subtitleStreamIndex: streamModel?.defaultSubStreamIndex, enableTranscoding: true, autoOpenLiveStream: true, - deviceProfile: defaultProfile, + deviceProfile: ref.read(videoProfileProvider), userId: userId, mediaSourceId: firstItemToPlay.id, ), @@ -218,7 +230,7 @@ class PlaybackModelHelper { chapters: chapters, playbackInfo: playbackInfo, trickPlay: trickPlay, - media: Media('${ref.read(userProvider)?.server ?? ""}/Videos/${mediaSource.id}/stream?$params'), + media: Media(url: '${ref.read(userProvider)?.server ?? ""}/Videos/${mediaSource.id}/stream?$params'), mediaStreams: mediaStreamsWithUrls, ); } else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) { @@ -229,7 +241,7 @@ class PlaybackModelHelper { chapters: chapters, trickPlay: trickPlay, playbackInfo: playbackInfo, - media: Media("${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"), + media: Media(url: "${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"), mediaStreams: mediaStreamsWithUrls, ); } @@ -300,7 +312,7 @@ class PlaybackModelHelper { subtitleStreamIndex: subIndex, enableTranscoding: true, autoOpenLiveStream: true, - deviceProfile: defaultProfile, + deviceProfile: ref.read(videoProfileProvider), userId: userId, mediaSourceId: item.id, ), @@ -347,7 +359,7 @@ class PlaybackModelHelper { chapters: playbackModel.chapters, playbackInfo: playbackInfo, trickPlay: playbackModel.trickPlay, - media: Media(directPlay), + media: Media(url: directPlay), mediaStreams: mediaStreamsWithUrls, ); } else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) { @@ -358,7 +370,7 @@ class PlaybackModelHelper { chapters: playbackModel.chapters, playbackInfo: playbackInfo, trickPlay: playbackModel.trickPlay, - media: Media("${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"), + media: Media(url: "${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"), mediaStreams: mediaStreamsWithUrls, ); } diff --git a/lib/models/playback/transcode_playback_model.dart b/lib/models/playback/transcode_playback_model.dart index 9fb520c..38a7c30 100644 --- a/lib/models/playback/transcode_playback_model.dart +++ b/lib/models/playback/transcode_playback_model.dart @@ -1,6 +1,7 @@ +import 'package:flutter/widgets.dart'; + import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:media_kit/media_kit.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -13,9 +14,7 @@ import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/util/duration_extensions.dart'; import 'package:fladder/util/list_extensions.dart'; -import 'package:fladder/wrappers/media_control_wrapper.dart' - if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart'; -import 'package:flutter/widgets.dart'; +import 'package:fladder/wrappers/media_control_wrapper.dart'; class TranscodePlaybackModel implements PlaybackModel { TranscodePlaybackModel({ @@ -67,22 +66,8 @@ class TranscodePlaybackModel implements PlaybackModel { @override Future setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async { - final wantedSubtitle = - model ?? subStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultSubStreamIndex); - if (wantedSubtitle == null) return this; - if (wantedSubtitle.index == SubStreamModel.no().index) { - await player.setSubtitleTrack(SubtitleTrack.no()); - } else { - final subTracks = player.subTracks.getRange(2, player.subTracks.length).toList(); - final index = subStreams.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id); - final subTrack = subTracks.elementAtOrNull(index); - if (wantedSubtitle.isExternal && wantedSubtitle.url != null && subTrack == null) { - await player.setSubtitleTrack(SubtitleTrack.uri(wantedSubtitle.url!)); - } else if (subTrack != null) { - await player.setSubtitleTrack(subTrack); - } - } - return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: wantedSubtitle.index)); + final newIndex = await player.setSubtitleTrack(model, this); + return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: newIndex)); } @override @@ -90,19 +75,8 @@ class TranscodePlaybackModel implements PlaybackModel { @override Future? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async { - final wantedAudioStream = - model ?? audioStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultAudioStreamIndex); - if (wantedAudioStream == null) return this; - if (wantedAudioStream.index == AudioStreamModel.no().index) { - await player.setAudioTrack(AudioTrack.no()); - } else { - final audioTracks = player.audioTracks.getRange(2, player.audioTracks.length).toList(); - final audioTrack = audioTracks.elementAtOrNull(audioStreams.indexOf(wantedAudioStream) - 1); - if (audioTrack != null) { - await player.setAudioTrack(audioTrack); - } - } - return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: wantedAudioStream.index)); + final newIndex = await player.setAudioTrack(model, this); + return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: newIndex)); } @override diff --git a/lib/models/settings/video_player_settings.dart b/lib/models/settings/video_player_settings.dart index bea5820..6aa3d3a 100644 --- a/lib/models/settings/video_player_settings.dart +++ b/lib/models/settings/video_player_settings.dart @@ -20,6 +20,7 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { @Default(false) bool fillScreen, @Default(true) bool hardwareAccel, @Default(false) bool useLibass, + PlayerOptions? playerOptions, @Default(100) double internalVolume, Set? allowedOrientations, @Default(AutoNextType.smart) AutoNextType nextVideoType, @@ -33,8 +34,10 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { factory VideoPlayerSettingsModel.fromJson(Map json) => _$VideoPlayerSettingsModelFromJson(json); + PlayerOptions get wantedPlayer => playerOptions ?? PlayerOptions.platformDefaults; + bool playerSame(VideoPlayerSettingsModel other) { - return other.hardwareAccel == hardwareAccel && other.useLibass == useLibass; + return other.hardwareAccel == hardwareAccel && other.useLibass == useLibass && other.wantedPlayer == wantedPlayer; } @override @@ -48,6 +51,7 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { other.hardwareAccel == hardwareAccel && other.useLibass == useLibass && other.internalVolume == internalVolume && + other.playerOptions == playerOptions && other.audioDevice == audioDevice; } @@ -63,6 +67,27 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { } } +enum PlayerOptions { + libMDK, + libMPV; + + const PlayerOptions(); + + static Iterable get available => kIsWeb ? {PlayerOptions.libMPV} : PlayerOptions.values; + + static PlayerOptions get platformDefaults { + if (kIsWeb) return PlayerOptions.libMPV; + return switch (defaultTargetPlatform) { + _ => PlayerOptions.libMPV, + }; + } + + String label(BuildContext context) => switch (this) { + PlayerOptions.libMDK => "MDK", + PlayerOptions.libMPV => "MPV", + }; +} + enum AutoNextType { off, smart, diff --git a/lib/models/settings/video_player_settings.freezed.dart b/lib/models/settings/video_player_settings.freezed.dart index b755e2c..04f07ed 100644 --- a/lib/models/settings/video_player_settings.freezed.dart +++ b/lib/models/settings/video_player_settings.freezed.dart @@ -26,6 +26,7 @@ mixin _$VideoPlayerSettingsModel { bool get fillScreen => throw _privateConstructorUsedError; bool get hardwareAccel => throw _privateConstructorUsedError; bool get useLibass => throw _privateConstructorUsedError; + PlayerOptions? get playerOptions => throw _privateConstructorUsedError; double get internalVolume => throw _privateConstructorUsedError; Set? get allowedOrientations => throw _privateConstructorUsedError; @@ -54,6 +55,7 @@ abstract class $VideoPlayerSettingsModelCopyWith<$Res> { bool fillScreen, bool hardwareAccel, bool useLibass, + PlayerOptions? playerOptions, double internalVolume, Set? allowedOrientations, AutoNextType nextVideoType, @@ -81,6 +83,7 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res, Object? fillScreen = null, Object? hardwareAccel = null, Object? useLibass = null, + Object? playerOptions = freezed, Object? internalVolume = null, Object? allowedOrientations = freezed, Object? nextVideoType = null, @@ -107,6 +110,10 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res, ? _value.useLibass : useLibass // ignore: cast_nullable_to_non_nullable as bool, + playerOptions: freezed == playerOptions + ? _value.playerOptions + : playerOptions // ignore: cast_nullable_to_non_nullable + as PlayerOptions?, internalVolume: null == internalVolume ? _value.internalVolume : internalVolume // ignore: cast_nullable_to_non_nullable @@ -142,6 +149,7 @@ abstract class _$$VideoPlayerSettingsModelImplCopyWith<$Res> bool fillScreen, bool hardwareAccel, bool useLibass, + PlayerOptions? playerOptions, double internalVolume, Set? allowedOrientations, AutoNextType nextVideoType, @@ -168,6 +176,7 @@ class __$$VideoPlayerSettingsModelImplCopyWithImpl<$Res> Object? fillScreen = null, Object? hardwareAccel = null, Object? useLibass = null, + Object? playerOptions = freezed, Object? internalVolume = null, Object? allowedOrientations = freezed, Object? nextVideoType = null, @@ -194,6 +203,10 @@ class __$$VideoPlayerSettingsModelImplCopyWithImpl<$Res> ? _value.useLibass : useLibass // ignore: cast_nullable_to_non_nullable as bool, + playerOptions: freezed == playerOptions + ? _value.playerOptions + : playerOptions // ignore: cast_nullable_to_non_nullable + as PlayerOptions?, internalVolume: null == internalVolume ? _value.internalVolume : internalVolume // ignore: cast_nullable_to_non_nullable @@ -224,6 +237,7 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel this.fillScreen = false, this.hardwareAccel = true, this.useLibass = false, + this.playerOptions, this.internalVolume = 100, final Set? allowedOrientations, this.nextVideoType = AutoNextType.smart, @@ -249,6 +263,8 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel @JsonKey() final bool useLibass; @override + final PlayerOptions? playerOptions; + @override @JsonKey() final double internalVolume; final Set? _allowedOrientations; @@ -270,7 +286,7 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, audioDevice: $audioDevice)'; + return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, audioDevice: $audioDevice)'; } @override @@ -283,6 +299,7 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel ..add(DiagnosticsProperty('fillScreen', fillScreen)) ..add(DiagnosticsProperty('hardwareAccel', hardwareAccel)) ..add(DiagnosticsProperty('useLibass', useLibass)) + ..add(DiagnosticsProperty('playerOptions', playerOptions)) ..add(DiagnosticsProperty('internalVolume', internalVolume)) ..add(DiagnosticsProperty('allowedOrientations', allowedOrientations)) ..add(DiagnosticsProperty('nextVideoType', nextVideoType)) @@ -313,6 +330,7 @@ abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel { final bool fillScreen, final bool hardwareAccel, final bool useLibass, + final PlayerOptions? playerOptions, final double internalVolume, final Set? allowedOrientations, final AutoNextType nextVideoType, @@ -333,6 +351,8 @@ abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel { @override bool get useLibass; @override + PlayerOptions? get playerOptions; + @override double get internalVolume; @override Set? get allowedOrientations; diff --git a/lib/models/settings/video_player_settings.g.dart b/lib/models/settings/video_player_settings.g.dart index 49b1fb5..2fd1260 100644 --- a/lib/models/settings/video_player_settings.g.dart +++ b/lib/models/settings/video_player_settings.g.dart @@ -15,6 +15,8 @@ _$VideoPlayerSettingsModelImpl _$$VideoPlayerSettingsModelImplFromJson( fillScreen: json['fillScreen'] as bool? ?? false, hardwareAccel: json['hardwareAccel'] as bool? ?? true, useLibass: json['useLibass'] as bool? ?? false, + playerOptions: + $enumDecodeNullable(_$PlayerOptionsEnumMap, json['playerOptions']), internalVolume: (json['internalVolume'] as num?)?.toDouble() ?? 100, allowedOrientations: (json['allowedOrientations'] as List?) ?.map((e) => $enumDecode(_$DeviceOrientationEnumMap, e)) @@ -33,6 +35,7 @@ Map _$$VideoPlayerSettingsModelImplToJson( 'fillScreen': instance.fillScreen, 'hardwareAccel': instance.hardwareAccel, 'useLibass': instance.useLibass, + 'playerOptions': _$PlayerOptionsEnumMap[instance.playerOptions], 'internalVolume': instance.internalVolume, 'allowedOrientations': instance.allowedOrientations ?.map((e) => _$DeviceOrientationEnumMap[e]!) @@ -51,6 +54,11 @@ const _$BoxFitEnumMap = { BoxFit.scaleDown: 'scaleDown', }; +const _$PlayerOptionsEnumMap = { + PlayerOptions.libMDK: 'libMDK', + PlayerOptions.libMPV: 'libMPV', +}; + const _$DeviceOrientationEnumMap = { DeviceOrientation.portraitUp: 'portraitUp', DeviceOrientation.landscapeLeft: 'landscapeLeft', diff --git a/lib/models/video_stream_model.dart b/lib/models/video_stream_model.dart index eb1b154..cb57e64 100644 --- a/lib/models/video_stream_model.dart +++ b/lib/models/video_stream_model.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:ficonsax/ficonsax.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:media_kit/media_kit.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/items/chapters_model.dart'; @@ -73,8 +72,6 @@ class VideoPlayback { ItemStreamModel? currentItem, SyncedItem? currentSyncedItem, VideoStream? currentStream, - Map? audioMappings, - Map? subMappings, }) { return VideoPlayback( queue: queue ?? this.queue, diff --git a/lib/profiles/default_profile.dart b/lib/profiles/default_profile.dart index 60a34e6..95ce9e0 100644 --- a/lib/profiles/default_profile.dart +++ b/lib/profiles/default_profile.dart @@ -1,23 +1,31 @@ -import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; -import 'package:fladder/profiles/web_profile.dart'; import 'package:flutter/foundation.dart'; -const DeviceProfile defaultProfile = kIsWeb +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; +import 'package:fladder/models/settings/video_player_settings.dart'; +import 'package:fladder/profiles/web_profile.dart'; +import 'package:fladder/providers/video_player_provider.dart'; + +final videoProfileProvider = StateProvider.autoDispose((ref) => + defaultProfile(ref.read(videoPlayerProvider.select((value) => value.backend)) ?? PlayerOptions.platformDefaults)); + +DeviceProfile defaultProfile(PlayerOptions player) => kIsWeb ? webProfile : DeviceProfile( maxStreamingBitrate: 120000000, maxStaticBitrate: 120000000, musicStreamingTranscodingBitrate: 384000, directPlayProfiles: [ - DirectPlayProfile( + const DirectPlayProfile( type: DlnaProfileType.video, ), - DirectPlayProfile( + const DirectPlayProfile( type: DlnaProfileType.audio, ) ], transcodingProfiles: [ - TranscodingProfile( + const TranscodingProfile( audioCodec: 'aac,mp3,mp2', container: 'ts', maxAudioChannels: '2', @@ -28,8 +36,10 @@ const DeviceProfile defaultProfile = kIsWeb ], containerProfiles: [], subtitleProfiles: [ - SubtitleProfile(format: 'vtt', method: SubtitleDeliveryMethod.$external), - SubtitleProfile(format: 'ass', method: SubtitleDeliveryMethod.$external), - SubtitleProfile(format: 'ssa', method: SubtitleDeliveryMethod.$external), + const SubtitleProfile(format: 'vtt', method: SubtitleDeliveryMethod.$external), + const SubtitleProfile(format: 'ass', method: SubtitleDeliveryMethod.$external), + const SubtitleProfile(format: 'ssa', method: SubtitleDeliveryMethod.$external), + if (player == PlayerOptions.libMDK) + const SubtitleProfile(format: 'pgssub', method: SubtitleDeliveryMethod.$external), ], ); diff --git a/lib/providers/items/season_details_provider.dart b/lib/providers/items/season_details_provider.dart index e24b8db..53eb070 100644 --- a/lib/providers/items/season_details_provider.dart +++ b/lib/providers/items/season_details_provider.dart @@ -31,10 +31,7 @@ class SeasonDetailsNotifier extends StateNotifier { season: newState?.season, fields: [ItemFields.overview], ); - newState = newState?.copyWith( - episodes: EpisodeModel.episodesFromDto(episodes.body?.items, ref) - .where((element) => element.season == newState?.season) - .toList()); + newState = newState?.copyWith(episodes: EpisodeModel.episodesFromDto(episodes.body?.items, ref).toList()); state = newState; return season; } diff --git a/lib/providers/sync_provider.dart b/lib/providers/sync_provider.dart index 78231ee..abb68bb 100644 --- a/lib/providers/sync_provider.dart +++ b/lib/providers/sync_provider.dart @@ -420,11 +420,11 @@ class SyncNotifier extends StateNotifier { final playbackResponse = await api.itemsItemIdPlaybackInfoPost( itemId: syncItem.id, - body: const PlaybackInfoDto( + body: PlaybackInfoDto( enableDirectPlay: true, enableDirectStream: true, enableTranscoding: false, - deviceProfile: defaultProfile, + deviceProfile: ref.read(videoProfileProvider), ), ); diff --git a/lib/providers/video_player_provider.dart b/lib/providers/video_player_provider.dart index 3364ef0..b61ed52 100644 --- a/lib/providers/video_player_provider.dart +++ b/lib/providers/video_player_provider.dart @@ -1,15 +1,11 @@ import 'dart:async'; -import 'package:collection/collection.dart'; +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/providers/settings/video_player_settings_provider.dart'; -import 'package:fladder/wrappers/media_control_wrapper.dart' - if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:media_kit/media_kit.dart'; +import 'package:fladder/wrappers/media_control_wrapper.dart'; final mediaPlaybackProvider = StateProvider((ref) => MediaPlaybackModel()); @@ -30,45 +26,37 @@ class VideoPlayerNotifier extends StateNotifier { late final mediaState = ref.read(mediaPlaybackProvider.notifier); - bool initMediaControls = false; + MediaPlaybackModel get playbackState => ref.read(mediaPlaybackProvider); void init() async { - state.player?.dispose(); - if (!initMediaControls && !kDebugMode) { - state.init(); - initMediaControls = true; - } + await state.dispose(); + await state.init(); + for (final s in subscriptions) { s.cancel(); } - final player = state.setup(); - if (player.platform is NativePlayer) { - await (player.platform as dynamic).setProperty( - 'force-seekable', - 'yes', - ); + + final subscription = state.stateStream?.listen((value) { + mediaState.update((state) => state.copyWith(buffering: value.buffering)); + mediaState.update((state) => state.copyWith(buffer: value.buffer)); + updatePlaying(value.playing); + updatePosition(value.position); + mediaState.update((state) => state.copyWith(duration: value.duration)); + }); + + if (subscription != null) { + subscriptions.add(subscription); } - subscriptions.addAll( - [ - player.stream.buffering.listen((event) => mediaState.update((state) => state.copyWith(buffering: event))), - player.stream.buffer.listen((event) => mediaState.update((state) => state.copyWith(buffer: event))), - player.stream.playing.listen((event) => updatePlaying(event)), - player.stream.position.listen((event) => updatePosition(event)), - player.stream.duration.listen((event) => mediaState.update((state) => state.copyWith(duration: event))), - ].whereNotNull(), - ); } Future updatePlaying(bool event) async { - final player = state.player; - if (player == null) return; + if (!state.hasPlayer) return; mediaState.update((state) => state.copyWith(playing: event)); } Future updatePosition(Duration event) async { - final player = state.player; - if (player == null) return; - if (!player.state.playing) return; + if (!state.hasPlayer) return; + if (playbackState.playing == false) return; final position = event; final lastPosition = ref.read(mediaPlaybackProvider.select((value) => value.lastPosition)); @@ -77,16 +65,16 @@ class VideoPlayerNotifier extends StateNotifier { if (diff > const Duration(seconds: 1, milliseconds: 500).inMilliseconds) { mediaState.update((value) => value.copyWith( position: event, - playing: player.state.playing, - duration: player.state.duration, + playing: playbackState.playing, + duration: playbackState.duration, lastPosition: position, )); - ref.read(playBackModel)?.updatePlaybackPosition(position, player.state.playing, ref); + ref.read(playBackModel)?.updatePlaybackPosition(position, playbackState.playing, ref); } else { mediaState.update((value) => value.copyWith( position: event, - playing: player.state.playing, - duration: player.state.duration, + playing: playbackState.playing, + duration: playbackState.duration, )); } } @@ -100,22 +88,24 @@ class VideoPlayerNotifier extends StateNotifier { PlaybackModel? newPlaybackModel = model; if (media != null) { + await state.open(media.url, false); await state.setVolume(ref.read(videoPlayerSettingsProvider).volume); - await state.open(media, play: false); - state.player?.stream.buffering.takeWhile((event) => event == true).listen( + state.stateStream?.takeWhile((event) => event.buffering == true).listen( null, onDone: () async { final start = startPosition ?? await model.startDuration(); if (start != null) { await state.seek(start); } - newPlaybackModel = await newPlaybackModel?.setAudio(null, state); - newPlaybackModel = await newPlaybackModel?.setSubtitle(null, state); - ref.read(playBackModel.notifier).update((state) => newPlaybackModel); + await state.setAudioTrack(null, model); + await state.setSubtitleTrack(null, model); state.play(); + ref.read(playBackModel.notifier).update((state) => newPlaybackModel); }, ); + ref.read(playBackModel.notifier).update((state) => model); + return true; } diff --git a/lib/screens/details_screens/components/media_stream_information.dart b/lib/screens/details_screens/components/media_stream_information.dart index 5262a1c..4493d31 100644 --- a/lib/screens/details_screens/components/media_stream_information.dart +++ b/lib/screens/details_screens/components/media_stream_information.dart @@ -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, diff --git a/lib/screens/login/login_user_grid.dart b/lib/screens/login/login_user_grid.dart index 7c4702a..1b0d1df 100644 --- a/lib/screens/login/login_user_grid.dart +++ b/lib/screens/login/login_user_grid.dart @@ -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, ), ); } diff --git a/lib/screens/photo_viewer/simple_video_player.dart b/lib/screens/photo_viewer/simple_video_player.dart index 0c0d845..b0bf1f9 100644 --- a/lib/screens/photo_viewer/simple_video_player.dart +++ b/lib/screens/photo_viewer/simple_video_player.dart @@ -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 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 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 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 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 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 with Wind } }, onChangeStart: (value) { - wasPlaying = player.state.playing; + wasPlaying = player.lastState.playing; player.pause(); }, onChanged: (e) { @@ -239,7 +228,7 @@ class _SimpleVideoPlayerState extends ConsumerState 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)) ], diff --git a/lib/screens/settings/player_settings_page.dart b/lib/screens/settings/player_settings_page.dart index 15bb2b7..aa27a85 100644 --- a/lib/screens/settings/player_settings_page.dart +++ b/lib/screens/settings/player_settings_page.dart @@ -82,34 +82,90 @@ class _PlayerSettingsPageState extends ConsumerState { ), 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 { _ => 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), diff --git a/lib/screens/settings/widgets/subtitle_editor.dart b/lib/screens/settings/widgets/subtitle_editor.dart index 9428aa6..9152202 100644 --- a/lib/screens/settings/widgets/subtitle_editor.dart +++ b/lib/screens/settings/widgets/subtitle_editor.dart @@ -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}); diff --git a/lib/screens/shared/flat_button.dart b/lib/screens/shared/flat_button.dart index 6bc0759..5eaea82 100644 --- a/lib/screens/shared/flat_button.dart +++ b/lib/screens/shared/flat_button.dart @@ -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, + ), + ), + ), + ], ); } } diff --git a/lib/screens/video_player/components/video_playback_information.dart b/lib/screens/video_player/components/video_playback_information.dart index ad8e6ba..b38cce7 100644 --- a/lib/screens/video_player/components/video_playback_information.dart +++ b/lib/screens/video_player/components/video_playback_information.dart @@ -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 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)) + ), ], ), ), diff --git a/lib/screens/video_player/components/video_player_controls_extras.dart b/lib/screens/video_player/components/video_player_controls_extras.dart index 385afc0..4d277c0 100644 --- a/lib/screens/video_player/components/video_player_controls_extras.dart +++ b/lib/screens/video_player/components/video_player_controls_extras.dart @@ -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( diff --git a/lib/screens/video_player/components/video_player_next_wrapper.dart b/lib/screens/video_player/components/video_player_next_wrapper.dart index e243195..3928f0a 100644 --- a/lib/screens/video_player/components/video_player_next_wrapper.dart +++ b/lib/screens/video_player/components/video_player_next_wrapper.dart @@ -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) diff --git a/lib/screens/video_player/components/video_player_options_sheet.dart b/lib/screens/video_player/components/video_player_options_sheet.dart index df01914..dd823ba 100644 --- a/lib/screens/video_player/components/video_player_options_sheet.dart +++ b/lib/screens/video_player/components/video_player_options_sheet.dart @@ -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 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 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 showPlaybackSpeed(BuildContext context) { divisions: 39, onChanged: (value) { ref.read(playbackRateProvider.notifier).state = value; - player?.setRate(value); + player.setSpeed(value); }, ), ), diff --git a/lib/screens/video_player/components/video_progress_bar.dart b/lib/screens/video_player/components/video_progress_bar.dart index 1845a3e..738ff02 100644 --- a/lib/screens/video_player/components/video_progress_bar.dart +++ b/lib/screens/video_player/components/video_progress_bar.dart @@ -122,7 +122,7 @@ class _ChapterProgressSliderState extends ConsumerState { setState(() { onHoverStart = true; }); - widget.wasPlayingChanged.call(player.player?.state.playing ?? false); + widget.wasPlayingChanged.call(player.lastState?.playing ?? false); player.pause(); }, onChanged: (e) { diff --git a/lib/screens/video_player/components/video_subtitles.dart b/lib/screens/video_player/components/video_subtitles.dart deleted file mode 100644 index a7fad31..0000000 --- a/lib/screens/video_player/components/video_subtitles.dart +++ /dev/null @@ -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 createState() => _VideoSubtitlesState(); -} - -class _VideoSubtitlesState extends ConsumerState { - late List subtitle = widget.controller.player.state.subtitle; - StreamSubscription>? 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, - ); - } - } -} diff --git a/lib/screens/video_player/video_player.dart b/lib/screens/video_player/video_player.dart index 183ac8f..56f91a5 100644 --- a/lib/screens/video_player/video_player.dart +++ b/lib/screens/video_player/video_player.dart @@ -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 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 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(), diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 921d5b6..eb3ef20 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -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 { @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 { 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 { 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 { ), ), ), - } + }, ].addPadding(const EdgeInsets.symmetric(horizontal: 4)), ), const SizedBox(height: 4), diff --git a/lib/stubs/web/lib_mdk_web.dart b/lib/stubs/web/lib_mdk_web.dart new file mode 100644 index 0000000..115ffb5 --- /dev/null +++ b/lib/stubs/web/lib_mdk_web.dart @@ -0,0 +1,75 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/models/items/media_streams_model.dart'; +import 'package:fladder/models/playback/playback_model.dart'; +import 'package:fladder/wrappers/players/base_player.dart'; +import 'package:fladder/wrappers/players/player_states.dart'; + +// This is a stub class that provides the same method signatures as the original +// implementation, ensuring web builds compile without requiring changes elsewhere. +class LibMDK extends BasePlayer { + final StreamController _stateController = StreamController.broadcast(); + + @override + Stream get stateStream => _stateController.stream; + + @override + Future init(Ref ref) async {} + + @override + Future dispose() async {} + + @override + Future open(String url, bool play) async {} + + void setState(PlayerState state) {} + + void updateState() {} + + @override + Future pause() async {} + + @override + Future play() async {} + @override + Future playOrPause() async {} + + @override + Future seek(Duration position) async {} + + @override + Future setAudioTrack(AudioStreamModel? model, PlaybackModel playbackModel) async { + return -1; + } + + @override + Future setSpeed(double speed) async {} + + @override + Future setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel) async { + return -1; + } + + @override + Future stop() async {} + + @override + Widget? videoWidget( + Key key, + BoxFit fit, + ) => + null; + + @override + Widget? subtitles(bool showOverlay) => null; + + @override + Future setVolume(double volume) async {} + + @override + Future loop(bool loop) async {} +} diff --git a/lib/stubs/web/smtc_web.dart b/lib/stubs/web/smtc_web.dart new file mode 100644 index 0000000..093d726 --- /dev/null +++ b/lib/stubs/web/smtc_web.dart @@ -0,0 +1,195 @@ +// ignore_for_file: implementation_imports, constant_identifier_names +import 'dart:async'; + +// This is a stub class that provides the same method signatures as the original +// implementation, ensuring web builds compile without requiring changes elsewhere. +class SMTCWindows { + SMTCWindows({ + SMTCConfig? config, + PlaybackTimeline? timeline, + MusicMetadata? metadata, + PlaybackStatus? status, + bool? shuffleEnabled, + RepeatMode? repeatMode, + bool? enabled, + }); + + get buttonPressStream => null; + + Future updateConfig(SMTCConfig config) async {} + + Future updateTimeline(PlaybackTimeline timeline) async {} + + Future updateMetadata(MusicMetadata metadata) async {} + + Future clearMetadata() async {} + + Future dispose() async {} + + Future disableSmtc() async {} + + Future enableSmtc() async {} + + Future setPlaybackStatus(PlaybackStatus status) async {} + + Future setIsPlayEnabled(bool enabled) async {} + + Future setIsPauseEnabled(bool enabled) async {} + + Future setIsStopEnabled(bool enabled) async {} + + Future setIsNextEnabled(bool enabled) async {} + + Future setIsPrevEnabled(bool enabled) async {} + + Future setIsFastForwardEnabled(bool enabled) async {} + + Future setIsRewindEnabled(bool enabled) async {} + + Future setTimeline(PlaybackTimeline timeline) async {} + + Future setTitle(String title) async {} + + Future setArtist(String artist) async {} + + Future setAlbum(String album) async {} + + Future setAlbumArtist(String albumArtist) async {} + + Future setThumbnail(String thumbnail) async {} + + Future setPosition(Duration position) async {} + + Future setStartTime(Duration startTime) async {} + + Future setEndTime(Duration endTime) async {} + + Future setMaxSeekTime(Duration maxSeekTime) async {} + + Future setMinSeekTime(Duration minSeekTime) async {} + + Future setShuffleEnabled(bool enabled) async {} + + Future setRepeatMode(RepeatMode repeatMode) async {} +} + +class MusicMetadata { + final String? title; + final String? artist; + final String? album; + final String? albumArtist; + final String? thumbnail; + + const MusicMetadata({ + this.title, + this.artist, + this.album, + this.albumArtist, + this.thumbnail, + }); +} + +enum PlaybackStatus { + Closed, + Changing, + Stopped, + Playing, + Paused, +} + +enum PressedButton { + play, + pause, + next, + previous, + fastForward, + rewind, + stop, + record, + channelUp, + channelDown; + + static PressedButton fromString(String button) { + switch (button) { + case 'play': + return PressedButton.play; + case 'pause': + return PressedButton.pause; + case 'next': + return PressedButton.next; + case 'previous': + return PressedButton.previous; + case 'fast_forward': + return PressedButton.fastForward; + case 'rewind': + return PressedButton.rewind; + case 'stop': + return PressedButton.stop; + case 'record': + return PressedButton.record; + case 'channel_up': + return PressedButton.channelUp; + case 'channel_down': + return PressedButton.channelDown; + default: + throw Exception('Unknown button: $button'); + } + } +} + +class PlaybackTimeline { + final int startTimeMs; + final int endTimeMs; + final int positionMs; + final int? minSeekTimeMs; + final int? maxSeekTimeMs; + + const PlaybackTimeline({ + required this.startTimeMs, + required this.endTimeMs, + required this.positionMs, + this.minSeekTimeMs, + this.maxSeekTimeMs, + }); +} + +class SMTCConfig { + final bool playEnabled; + final bool pauseEnabled; + final bool stopEnabled; + final bool nextEnabled; + final bool prevEnabled; + final bool fastForwardEnabled; + final bool rewindEnabled; + + const SMTCConfig({ + required this.playEnabled, + required this.pauseEnabled, + required this.stopEnabled, + required this.nextEnabled, + required this.prevEnabled, + required this.fastForwardEnabled, + required this.rewindEnabled, + }); +} + +enum RepeatMode { + none, + track, + list; + + static RepeatMode fromString(String value) { + switch (value) { + case 'none': + return none; + case 'track': + return track; + case 'list': + return list; + default: + throw Exception('Unknown repeat mode: $value'); + } + } + + String get asString => toString().split('.').last; +} diff --git a/lib/util/item_base_model/play_item_helpers.dart b/lib/util/item_base_model/play_item_helpers.dart index ad47b18..118e115 100644 --- a/lib/util/item_base_model/play_item_helpers.dart +++ b/lib/util/item_base_model/play_item_helpers.dart @@ -199,7 +199,7 @@ extension ItemBaseModelExtensions on ItemBaseModel? { switch (this) { PhotoAlbumModel album => album.play(context, ref), BookModel book => book.play(context, ref), - _ => _default(context, this, ref, startPosition: startPosition), + _ => _default(context, this, ref, startPosition: startPosition, showPlaybackOption: showPlaybackOption), }; Future _default( diff --git a/lib/util/player_extensions.dart b/lib/util/player_extensions.dart deleted file mode 100644 index 5130796..0000000 --- a/lib/util/player_extensions.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:fladder/models/items/media_streams_model.dart'; -import 'package:media_kit/media_kit.dart'; -import 'dart:io' show Platform; - -extension PlayerExtensions on Player { - Future addSubtitles(List subtitles) async { - final separator = Platform.isWindows ? ";" : ":"; - await (platform as NativePlayer).setProperty( - "sub-files", - subtitles - .mapIndexed((index, e) => "${Platform.isWindows ? e.url : e.url?.replaceFirst(":", "\\:")}@${e.displayTitle}") - .join(separator), - ); - } -} diff --git a/lib/util/player_extensions_web.dart b/lib/util/player_extensions_web.dart deleted file mode 100644 index ff0e63f..0000000 --- a/lib/util/player_extensions_web.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:fladder/models/items/media_streams_model.dart'; -import 'package:media_kit/media_kit.dart'; - -extension PlayerExtensions on Player { - Future addSubtitles(List subtitles) async {} -} diff --git a/lib/util/track_extensions.dart b/lib/util/track_extensions.dart deleted file mode 100644 index 822e06f..0000000 --- a/lib/util/track_extensions.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:media_kit/media_kit.dart'; -import 'package:validators/validators.dart'; -import 'string_extensions.dart'; - -extension SubtitleExtension on SubtitleTrack { - String get cleanName { - final names = { - id, - title, - }; - return names - .where((element) => element != null) - .map((e) { - if (e == null) return e; - if (isNumeric(e)) return ''; - if (e == "no") { - return "Off"; - } - return e.capitalize(); - }) - .where((element) => element != null && element.isNotEmpty) - .join(" - "); - } -} - -extension AudioTrackExtension on AudioTrack { - String get cleanName { - final names = { - id, - title, - }; - return names - .where((element) => element != null) - .map((e) { - if (e == null) return e; - if (isNumeric(e)) return ''; - if (e == "no") { - return "Off"; - } - return e.capitalize(); - }) - .where((element) => element != null && element.isNotEmpty) - .join(" - "); - } -} diff --git a/lib/widgets/navigation_scaffold/components/floating_player_bar.dart b/lib/widgets/navigation_scaffold/components/floating_player_bar.dart index 0e04589..e01ce2f 100644 --- a/lib/widgets/navigation_scaffold/components/floating_player_bar.dart +++ b/lib/widgets/navigation_scaffold/components/floating_player_bar.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:ficonsax/ficonsax.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:media_kit_video/media_kit_video.dart'; import 'package:window_manager/window_manager.dart'; import 'package:fladder/models/media_playback_model.dart'; @@ -103,12 +102,11 @@ class _CurrentlyPlayingBarState extends ConsumerState { children: [ Hero( tag: videoPlayerHeroTag, - child: Video( - controller: player.controller!, - fit: BoxFit.fitHeight, - controls: NoVideoControls, - wakelock: playbackInfo.playing, - ), + child: player.videoWidget( + UniqueKey(), + BoxFit.fitHeight, + ) ?? + const SizedBox.shrink(), ), Positioned.fill( child: Tooltip( @@ -169,8 +167,7 @@ class _CurrentlyPlayingBarState extends ConsumerState { if (constraints.maxWidth > 500) ...{ IconButton( onPressed: () { - final volume = player.player?.state.volume == 0 ? 100.0 : 0.0; - ref.read(videoPlayerSettingsProvider.notifier).setVolume(volume); + final volume = player.lastState?.volume == 0 ? 100.0 : 0.0; player.setVolume(volume); }, icon: Icon( diff --git a/lib/wrappers/media_control_base.dart b/lib/wrappers/media_control_base.dart deleted file mode 100644 index 8345aa7..0000000 --- a/lib/wrappers/media_control_base.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:audio_service/audio_service.dart'; -import 'package:media_kit/media_kit.dart'; - -AudioServiceConfig get audioServiceConfig => const AudioServiceConfig( - androidNotificationChannelId: 'nl.jknaapen.fladder.channel.playback', - androidNotificationChannelName: 'Video playback', - androidNotificationOngoing: true, - androidStopForegroundOnPause: true, - rewindInterval: Duration(seconds: 10), - fastForwardInterval: Duration(seconds: 15), - androidNotificationChannelDescription: "Playback", - androidShowNotificationBadge: true, - ); - -abstract class MediaControlBase { - Future init() { - throw UnimplementedError(); - } - - Player setup() { - throw UnimplementedError(); - } - - Future seek(Duration position) { - throw UnimplementedError(); - } - - Future play() { - throw UnimplementedError(); - } - - Future fastForward() { - throw UnimplementedError(); - } - - Future rewind() { - throw UnimplementedError(); - } - - Future setSpeed(double speed) { - throw UnimplementedError(); - } - - Future pause() { - throw UnimplementedError(); - } - - Future stop() { - throw UnimplementedError(); - } - - void playOrPause() { - throw UnimplementedError(); - } - - Future setSubtitleTrack(SubtitleTrack subtitleTrack) { - throw UnimplementedError(); - } - - Future setAudioTrack(AudioTrack subtitleTrack) { - throw UnimplementedError(); - } -} diff --git a/lib/wrappers/media_control_wrapper.dart b/lib/wrappers/media_control_wrapper.dart index c666bfc..8ec1137 100644 --- a/lib/wrappers/media_control_wrapper.dart +++ b/lib/wrappers/media_control_wrapper.dart @@ -2,76 +2,98 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:audio_service/audio_service.dart'; -import 'package:collection/collection.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:smtc_windows/smtc_windows.dart'; +import 'package:smtc_windows/smtc_windows.dart' if (dart.library.html) 'package:fladder/stubs/web/smtc_web.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:fladder/models/item_base_model.dart'; +import 'package:fladder/models/items/media_streams_model.dart'; +import 'package:fladder/models/playback/playback_model.dart'; +import 'package:fladder/models/settings/video_player_settings.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; -import 'package:fladder/wrappers/media_control_base.dart'; -import 'package:fladder/wrappers/media_wrapper_interface.dart'; +import 'package:fladder/wrappers/players/base_player.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'; +import 'package:fladder/wrappers/players/player_states.dart'; -class MediaControlsWrapper extends MediaPlayback implements MediaControlBase { +class MediaControlsWrapper extends BaseAudioHandler { MediaControlsWrapper({required this.ref}); + BasePlayer? _player; + + bool get hasPlayer => _player != null; + + PlayerOptions? get backend => switch (_player) { + LibMPV _ => PlayerOptions.libMPV, + LibMDK _ => PlayerOptions.libMDK, + _ => null, + }; + + Stream? get stateStream => _player?.stateStream; + PlayerState? get lastState => _player?.lastState; + + Widget? subtitleWidget(bool showOverlay) => _player?.subtitles(showOverlay); + Widget? videoWidget(Key key, BoxFit fit) => _player?.videoWidget(key, fit); + final Ref ref; List subscriptions = []; SMTCWindows? smtc; - @override + bool initMediaControls = false; + Future init() async { - await AudioService.init( - builder: () => this, - config: audioServiceConfig, - ); + if (!initMediaControls && !kDebugMode) { + await AudioService.init( + builder: () => this, + config: const AudioServiceConfig( + androidNotificationChannelId: 'nl.jknaapen.fladder.channel.playback', + androidNotificationChannelName: 'Video playback', + androidNotificationOngoing: true, + androidStopForegroundOnPause: true, + rewindInterval: Duration(seconds: 10), + fastForwardInterval: Duration(seconds: 15), + androidNotificationChannelDescription: "Playback", + androidShowNotificationBadge: true, + ), + ); + initMediaControls = true; + } + + final player = switch (ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer))) { + PlayerOptions.libMDK => LibMDK(), + PlayerOptions.libMPV => LibMPV(), + }; + + setup(player); } - @override - Player setup() => setPlayer(_initPlayer()); + Future dispose() async => _player?.dispose(); - Player _initPlayer() { + Future setup(BasePlayer newPlayer) async { + _player = newPlayer; + await newPlayer.init(ref); + _initPlayer(); + } + + void _initPlayer() { for (var element in subscriptions) { element.cancel(); } - stop(); - - player?.dispose(); - - final newPlayer = Player( - configuration: PlayerConfiguration( - title: "nl.jknaapen.fladder", - bufferSize: 64 * 1024 * 1024, - libassAndroidFont: 'assets/fonts/mp-font.ttf', - libass: !kIsWeb && - ref.read( - videoPlayerSettingsProvider.select((value) => value.useLibass), - ), - ), - ); - setPlayer(newPlayer); - setController(VideoController( - newPlayer, - configuration: VideoControllerConfiguration( - enableHardwareAcceleration: ref.read( - videoPlayerSettingsProvider.select((value) => value.hardwareAccel), - ), - ), - )); _subscribePlayer(); - return newPlayer; } + Future open(String url, bool play) async => _player?.open(url, play); + void _subscribePlayer() { - if (Platform.isWindows) { + if (Platform.isWindows && !kIsWeb) { smtc = SMTCWindows( config: const SMTCConfig( fastForwardEnabled: true, @@ -113,43 +135,33 @@ class MediaControlsWrapper extends MediaPlayback implements MediaControlBase { } } - subscriptions.addAll([ - player?.stream.buffer.listen((buffer) { - playbackState.add(playbackState.value.copyWith( - bufferedPosition: buffer, - )); - }), - player?.stream.buffering.listen((buffering) { - playbackState.add(playbackState.value.copyWith( - processingState: buffering ? AudioProcessingState.buffering : AudioProcessingState.ready, - )); - }), - player?.stream.position.listen((position) { - playbackState.add(playbackState.value.copyWith( - updatePosition: position, - )); - smtc?.setPosition(position); - }), - player?.stream.playing.listen((playing) { - if (playing) { - WakelockPlus.enable(); - } else { - WakelockPlus.disable(); - } - playbackState.add(playbackState.value.copyWith( - playing: playing, - )); - smtc?.setPlaybackStatus(playing ? PlaybackStatus.Playing : PlaybackStatus.Paused); - }), - ].whereNotNull()); + subscriptions.add(_player!.stateStream.listen((value) { + playbackState.add(playbackState.value.copyWith( + bufferedPosition: value.buffer, + )); + playbackState.add(playbackState.value.copyWith( + processingState: value.buffering ? AudioProcessingState.buffering : AudioProcessingState.ready, + )); + playbackState.add(playbackState.value.copyWith( + updatePosition: value.position, + )); + smtc?.setPosition(value.position); + if (value.playing) { + WakelockPlus.enable(); + } else { + WakelockPlus.disable(); + } + playbackState.add(playbackState.value.copyWith( + playing: value.playing, + )); + smtc?.setPlaybackStatus(value.playing ? PlaybackStatus.Playing : PlaybackStatus.Paused); + })); } @override Future play() async { - if (!ref.read(clientSettingsProvider).enableMediaKeys) { - await player?.play(); - return super.play(); - } + _player?.play(); + if (!ref.read(clientSettingsProvider).enableMediaKeys) return; final playBackItem = ref.read(playBackModel.select((value) => value?.item)); final currentPosition = await ref.read(playBackModel.select((value) => value?.startDuration())); @@ -182,7 +194,6 @@ class MediaControlsWrapper extends MediaPlayback implements MediaControlBase { processingState: AudioProcessingState.ready, )); - await player?.play(); return super.play(); } @@ -211,9 +222,11 @@ class MediaControlsWrapper extends MediaPlayback implements MediaControlBase { @override Future stop() async { WakelockPlus.disable(); - final position = player?.state.position; - final totalDuration = player?.state.duration; - await player?.stop(); + final position = _player?.lastState.position; + final totalDuration = _player?.lastState.duration; + super.stop(); + _player?.stop(); + ref.read(playBackModel)?.playbackStopped(position ?? Duration.zero, totalDuration, ref); ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(position: Duration.zero)); smtc?.setPlaybackStatus(PlaybackStatus.Stopped); @@ -229,16 +242,37 @@ class MediaControlsWrapper extends MediaPlayback implements MediaControlBase { return super.stop(); } - @override - void playOrPause() async { - await player?.playOrPause(); + Future playOrPause() async { + await _player?.playOrPause(); playbackState.add(playbackState.value.copyWith( - playing: player?.state.playing ?? false, + playing: _player?.lastState.playing ?? false, controls: [MediaControl.play], )); - final playerState = player; + final playerState = _player; if (playerState != null) { - ref.read(playBackModel)?.updatePlaybackPosition(playerState.state.position, playerState.state.playing, ref); + ref + .read(playBackModel) + ?.updatePlaybackPosition(playerState.lastState.position, playerState.lastState.playing, ref); } } + + Future setAudioTrack(AudioStreamModel? model, PlaybackModel playbackModel) async => + await _player?.setAudioTrack(model, playbackModel) ?? -1; + + Future setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel) async => + await _player?.setSubtitleTrack(model, playbackModel) ?? -1; + + Future setVolume(double speed) async => _player?.setVolume(speed); + + @override + Future seek(Duration position) { + _player?.seek(position); + return super.seek(position); + } + + @override + Future setSpeed(double speed) { + _player?.setSpeed(speed); + return super.setSpeed(speed); + } } diff --git a/lib/wrappers/media_control_wrapper_web.dart b/lib/wrappers/media_control_wrapper_web.dart deleted file mode 100644 index d87b801..0000000 --- a/lib/wrappers/media_control_wrapper_web.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'dart:async'; - -import 'package:audio_service/audio_service.dart'; -import 'package:collection/collection.dart'; -import 'package:fladder/providers/settings/client_settings_provider.dart'; -import 'package:fladder/providers/settings/video_player_settings_provider.dart'; -import 'package:fladder/providers/video_player_provider.dart'; -import 'package:fladder/wrappers/media_control_base.dart'; -import 'package:fladder/wrappers/media_wrapper_interface.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:wakelock_plus/wakelock_plus.dart'; - -class MediaControlsWrapper extends MediaPlayback implements MediaControlBase { - MediaControlsWrapper({required this.ref}); - - final Ref ref; - - List subscriptions = []; - - @override - Future init() async { - await AudioService.init( - builder: () => this, - config: audioServiceConfig, - ); - } - - @override - Player setup() => setPlayer(_initPlayer()); - - Player _initPlayer() { - for (var element in subscriptions) { - element.cancel(); - } - - stop(); - - player?.dispose(); - - final newPlayer = Player( - configuration: PlayerConfiguration( - title: "nl.jknaapen.fladder", - bufferSize: 64 * 1024 * 1024, - libassAndroidFont: 'assets/fonts/mp-font.ttf', - libass: ref.read( - videoPlayerSettingsProvider.select((value) => value.useLibass), - ), - ), - ); - setPlayer(newPlayer); - setController(VideoController( - newPlayer, - configuration: VideoControllerConfiguration( - enableHardwareAcceleration: ref.read( - videoPlayerSettingsProvider.select((value) => value.hardwareAccel), - ), - ), - )); - _subscribePlayer(); - return newPlayer; - } - - Future _subscribePlayer() async { - subscriptions.addAll([ - player?.stream.buffer.listen((buffer) { - playbackState.add(playbackState.value.copyWith( - bufferedPosition: buffer, - )); - }), - player?.stream.buffering.listen((buffering) { - playbackState.add(playbackState.value.copyWith( - processingState: buffering ? AudioProcessingState.buffering : AudioProcessingState.ready, - )); - }), - player?.stream.position.listen((position) { - playbackState.add(playbackState.value.copyWith( - updatePosition: position, - )); - }), - player?.stream.playing.listen((playing) { - playbackState.add(playbackState.value.copyWith( - playing: playing, - )); - }), - ].whereNotNull()); - } - - @override - Future seek(Duration position) async => player?.seek(position); - - @override - Future play() async { - if (!ref.read(clientSettingsProvider).enableMediaKeys) { - await player?.play(); - return super.play(); - } - - final playBackItem = ref.read(playBackModel.select((value) => value?.item)); - if (playBackItem == null) return; - - final poster = playBackItem.images?.firstOrNull; - - //Everything else setup - mediaItem.add(MediaItem( - id: playBackItem.id, - title: playBackItem.title, - artist: playBackItem.subText, - rating: Rating.newHeartRating(playBackItem.userData.isFavourite), - duration: playBackItem.overview.runTime ?? const Duration(seconds: 0), - artUri: poster != null ? Uri.parse(poster.path) : null, - )); - playbackState.add(playbackState.value.copyWith( - playing: true, - controls: [ - MediaControl.pause, - MediaControl.stop, - ], - systemActions: const { - MediaAction.seek, - MediaAction.fastForward, - MediaAction.setSpeed, - MediaAction.rewind, - }, - processingState: AudioProcessingState.ready, - )); - - await player?.play(); - return super.play(); - } - - @override - Future pause() async { - playbackState.add(playbackState.value.copyWith( - playing: false, - controls: [MediaControl.play], - )); - await player?.pause(); - return super.pause(); - } - - @override - Future stop() async { - WakelockPlus.disable(); - final position = player?.state.position; - final totalDuration = player?.state.duration; - await player?.stop(); - ref.read(playBackModel)?.playbackStopped(position ?? Duration.zero, totalDuration, ref); - ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(position: Duration.zero)); - - playbackState.add( - playbackState.value.copyWith( - playing: false, - processingState: AudioProcessingState.completed, - controls: [], - ), - ); - return super.stop(); - } - - @override - void playOrPause() { - player?.playOrPause(); - playbackState.add(playbackState.value.copyWith( - playing: player?.state.playing ?? false, - controls: [MediaControl.play], - )); - } -} diff --git a/lib/wrappers/media_wrapper_interface.dart b/lib/wrappers/media_wrapper_interface.dart deleted file mode 100644 index fd59c07..0000000 --- a/lib/wrappers/media_wrapper_interface.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:audio_service/audio_service.dart'; -import 'package:media_kit/media_kit.dart'; -import 'package:media_kit_video/media_kit_video.dart'; - -class MediaPlayback extends BaseAudioHandler { - Player? _player; - VideoController? _controller; - Player? get player => _player; - VideoController? get controller => _controller; - - Player setPlayer(Player player) => _player = player; - VideoController setController(VideoController player) => _controller = player; - - Future setVolume(double volume) async => _player?.setVolume(volume); - - Future setSubtitleTrack(SubtitleTrack track) async => _player?.setSubtitleTrack(track); - List get subTracks => _player?.state.tracks.subtitle ?? []; - SubtitleTrack get subtitleTrack => _player?.state.track.subtitle ?? SubtitleTrack.no(); - - Future setAudioTrack(AudioTrack track) async => _player?.setAudioTrack(track); - List get audioTracks => _player?.state.tracks.audio ?? []; - AudioTrack get audioTrack => _player?.state.track.audio ?? AudioTrack.no(); - - @override - Future seek(Duration position) async => player?.seek(position); - - @override - Future play() async { - await player?.play(); - return super.play(); - } - - Future open( - Playable playable, { - bool play = true, - }) async { - return player?.open(playable, play: play); - } - - @override - Future fastForward() async { - if (player != null) { - await player!.seek(player!.state.position + const Duration(seconds: 30)); - } - return super.fastForward(); - } - - @override - Future rewind() async { - if (player != null) { - await player?.seek(player!.state.position - const Duration(seconds: 10)); - } - return super.rewind(); - } - - @override - Future setSpeed(double speed) async { - await player?.setRate(speed); - return super.setSpeed(speed); - } - - @override - Future pause() async { - playbackState.add(playbackState.value.copyWith( - playing: false, - controls: [MediaControl.play], - )); - await player?.pause(); - return super.pause(); - } -} diff --git a/lib/wrappers/players/base_player.dart b/lib/wrappers/players/base_player.dart new file mode 100644 index 0000000..9536156 --- /dev/null +++ b/lib/wrappers/players/base_player.dart @@ -0,0 +1,50 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/models/items/media_streams_model.dart'; +import 'package:fladder/models/playback/playback_model.dart'; +import 'package:fladder/wrappers/players/player_states.dart'; + +const libassFallbackFont = "assets/mp-font.ttf"; + +abstract class BasePlayer { + Stream get stateStream; + PlayerState lastState = PlayerState(); + + Future init(Ref ref); + Widget? videoWidget( + Key key, + BoxFit fit, + ); + Widget? subtitles( + bool showOverlay, + ); + Future dispose(); + Future open(String url, bool play); + Future seek(Duration position); + Future play(); + Future setVolume(double volume); + Future setSpeed(double speed); + Future pause(); + Future stop(); + Future playOrPause(); + Future loop(bool loop); + Future setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel); + Future setAudioTrack(AudioStreamModel? model, PlaybackModel playbackModel); + + Uri? isValidUrl(String input) { + try { + final uri = Uri.tryParse(input); + if (uri != null && uri.isAbsolute && (uri.scheme == 'http' || uri.scheme == 'https')) { + return uri; + } else { + return null; + } + } catch (e) { + return null; + } + } +} diff --git a/lib/wrappers/players/lib_mdk.dart b/lib/wrappers/players/lib_mdk.dart new file mode 100644 index 0000000..c3d26bb --- /dev/null +++ b/lib/wrappers/players/lib_mdk.dart @@ -0,0 +1,183 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fvp/fvp.dart' as fvp; +import 'package:fvp/mdk.dart'; +import 'package:video_player/video_player.dart'; + +import 'package:fladder/models/items/media_streams_model.dart'; +import 'package:fladder/models/playback/playback_model.dart'; +import 'package:fladder/wrappers/players/base_player.dart'; +import 'package:fladder/wrappers/players/player_states.dart'; + +class LibMDK extends BasePlayer { + VideoPlayerController? _controller; + late final player = Player(); + + bool externalSubEnabled = false; + + final StreamController _stateController = StreamController.broadcast(); + + @override + Stream get stateStream => _stateController.stream; + + @override + Future init(Ref ref) async { + dispose(); + fvp.registerWith(options: { + 'global': {'log': 'off'}, + 'subtitleFontFile': libassFallbackFont, + }); + } + + @override + Future dispose() async { + _controller?.dispose(); + _controller = null; + } + + @override + Future open(String url, bool play) async { + final validUrl = isValidUrl(url); + if (validUrl != null) { + _controller = VideoPlayerController.networkUrl(validUrl); + } else { + _controller = VideoPlayerController.file(File(url)); + } + await _controller?.initialize(); + + _controller?.addListener(() => updateState()); + + if (play) { + await _controller?.play(); + } + return setState(lastState.update( + buffering: true, + )); + } + + void setState(PlayerState state) { + lastState = state; + _stateController.add(state); + } + + void updateState() { + setState(lastState.update( + playing: _controller?.value.isPlaying ?? false, + completed: _controller?.value.isCompleted ?? false, + position: _controller?.value.position ?? Duration.zero, + duration: _controller?.value.duration ?? Duration.zero, + volume: (_controller?.value.volume ?? 1.0) * 100, + rate: _controller?.value.playbackSpeed ?? 1.0, + buffering: _controller?.value.isBuffering ?? true, + buffer: _controller?.value.buffered.last.end ?? Duration.zero, + )); + } + + @override + Future pause() async => _controller?.pause(); + @override + Future play() async => _controller?.play(); + @override + Future playOrPause() async => lastState.playing ? _controller?.pause() : _controller?.play(); + + @override + Future seek(Duration position) async => _controller?.seekTo(position); + + @override + Future setAudioTrack(AudioStreamModel? model, PlaybackModel playbackModel) async { + final wantedAudioStream = model ?? playbackModel.defaultAudioStream; + if (wantedAudioStream == AudioStreamModel.no() || wantedAudioStream == null) { + _controller?.setAudioTracks([-1]); + return -1; + } else { + final indexOf = playbackModel.audioStreams?.indexOf(wantedAudioStream); + if (indexOf != null) { + _controller?.setAudioTracks([indexOf - 1]); + } + return wantedAudioStream.index; + } + } + + @override + Future setSpeed(double speed) async => _controller?.setPlaybackSpeed(speed); + + @override + Future setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel) async { + final wantedSubtitle = model ?? playbackModel.defaultSubStream; + if (wantedSubtitle == SubStreamModel.no()) { + externalSubEnabled = false; + _controller?.setSubtitleTracks([-1]); + return -1; + } + if (wantedSubtitle != null) { + if (wantedSubtitle.isExternal && wantedSubtitle.url != null) { + externalSubEnabled = true; + _controller?.setExternalSubtitle(wantedSubtitle.url!); + return wantedSubtitle.index; + } else { + if (externalSubEnabled) { + externalSubEnabled = false; + _controller?.setExternalSubtitle(""); + } + final indexOf = playbackModel.subStreams?.indexOf(wantedSubtitle); + if (indexOf != null) { + _controller?.setSubtitleTracks([indexOf - 1]); + } + return wantedSubtitle.index; + } + } + return -1; + } + + @override + Future stop() async => _controller?.dispose(); + + @override + Widget? videoWidget( + Key key, + BoxFit fit, + ) => + _controller == null + ? null + : Container( + key: key, + color: Colors.transparent, + child: LayoutBuilder( + builder: (context, constraints) => Stack( + fit: StackFit.expand, + children: [ + FittedBox( + fit: fit, + alignment: Alignment.center, + child: ValueListenableBuilder( + valueListenable: _controller!, + builder: (context, value, child) { + final aspectRatio = value.isInitialized ? value.aspectRatio : 1.77; + return SizedBox( + width: constraints.maxWidth, + child: AspectRatio( + aspectRatio: aspectRatio, + child: VideoPlayer(_controller!), + ), + ); + }, + ), + ), + ], + ), + ), + ); + + @override + Widget? subtitles(bool showOverlay) => null; + + @override + Future setVolume(double volume) async => _controller?.setVolume(volume / 100); + + @override + Future loop(bool loop) async => _controller?.setLooping(loop); +} diff --git a/lib/wrappers/players/lib_mpv.dart b/lib/wrappers/players/lib_mpv.dart new file mode 100644 index 0000000..9773cd4 --- /dev/null +++ b/lib/wrappers/players/lib_mpv.dart @@ -0,0 +1,251 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:media_kit/media_kit.dart' as mpv; +import 'package:media_kit_video/media_kit_video.dart'; + +import 'package:fladder/models/items/media_streams_model.dart'; +import 'package:fladder/models/playback/playback_model.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'; +import 'package:fladder/wrappers/players/base_player.dart'; +import 'package:fladder/wrappers/players/player_states.dart'; + +class LibMPV extends BasePlayer { + mpv.Player? _player; + VideoController? _controller; + + final StreamController _stateController = StreamController.broadcast(); + @override + Stream get stateStream => _stateController.stream; + + StreamSubscription? _onCompleted; + + @override + Future init(Ref ref) async { + dispose(); + + mpv.MediaKit.ensureInitialized(); + + _player = mpv.Player( + configuration: mpv.PlayerConfiguration( + title: "nl.jknaapen.fladder", + libassAndroidFont: libassFallbackFont, + libass: !kIsWeb && + ref.read( + videoPlayerSettingsProvider.select((value) => value.useLibass), + ), + ), + ); + + if (_player != null) { + _controller = VideoController( + _player!, + configuration: VideoControllerConfiguration( + enableHardwareAcceleration: ref.read( + videoPlayerSettingsProvider.select((value) => value.hardwareAccel), + ), + ), + ); + + _player!.stream.playing.listen((value) => setState(lastState.update(playing: value))); + _player!.stream.buffering.listen((value) => setState(lastState.update(buffering: value))); + _player!.stream.position.listen((value) => setState(lastState.update(position: value))); + _player!.stream.duration.listen((value) => setState(lastState.update(duration: value))); + _player!.stream.volume.listen((value) => setState(lastState.update(volume: value))); + _player!.stream.rate.listen((value) => setState(lastState.update(rate: value))); + _player!.stream.buffer.listen((value) => setState(lastState.update(buffer: value))); + } + + if (_player?.platform is mpv.NativePlayer) { + await (_player?.platform as dynamic).setProperty( + 'force-seekable', + 'yes', + ); + } + } + + @override + Future dispose() async { + _onCompleted?.cancel(); + _onCompleted = null; + _player?.stop(); + _player?.dispose(); + _player = null; + } + + void setState(PlayerState state) { + lastState = state; + _stateController.add(state); + } + + @override + Future open(String url, bool play) async { + await _player?.open(mpv.Media(url), play: play); + return setState(lastState.update(buffering: true)); + } + + List get subTracks => _player?.state.tracks.subtitle ?? []; + mpv.SubtitleTrack get subtitleTrack => _player?.state.track.subtitle ?? mpv.SubtitleTrack.no(); + + List get audioTracks => _player?.state.tracks.audio ?? []; + mpv.AudioTrack get audioTrack => _player?.state.track.audio ?? mpv.AudioTrack.no(); + + @override + Future pause() async => _player?.pause(); + + @override + Future play() async => _player?.play(); + + @override + Future playOrPause() async => _player?.playOrPause(); + + @override + Future seek(Duration position) async => _player?.seek(position); + + @override + Future setAudioTrack(AudioStreamModel? model, PlaybackModel playbackModel) async { + final wantedAudioStream = model ?? playbackModel.defaultAudioStream; + if (wantedAudioStream == null) return -1; + if (wantedAudioStream.index == AudioStreamModel.no().index) { + await _player?.setAudioTrack(mpv.AudioTrack.no()); + } else { + final internalTracks = audioTracks.getRange(2, audioTracks.length).toList(); + final audioTrack = + internalTracks.elementAtOrNull((playbackModel.audioStreams?.indexOf(wantedAudioStream) ?? -1) - 1); + if (audioTrack != null) { + await _player?.setAudioTrack(audioTrack); + } + } + return wantedAudioStream.index; + } + + @override + Future setSpeed(double speed) async => _player?.setRate(speed); + + @override + Future setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel) async { + if (_player == null) return -1; + final wantedSubtitle = model ?? playbackModel.defaultSubStream; + if (wantedSubtitle == null) return -1; + if (wantedSubtitle.index == SubStreamModel.no().index) { + await _player?.setSubtitleTrack(mpv.SubtitleTrack.no()); + } else { + final internalTrack = subTracks.getRange(2, subTracks.length).toList(); + final index = playbackModel.subStreams?.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id); + final subTrack = internalTrack.elementAtOrNull(index ?? -1); + if (wantedSubtitle.isExternal && wantedSubtitle.url != null && subTrack == null) { + await _player?.setSubtitleTrack(mpv.SubtitleTrack.uri(wantedSubtitle.url!)); + } else if (subTrack != null) { + await _player?.setSubtitleTrack(subTrack); + } + } + return wantedSubtitle.index; + } + + @override + Future stop() async => _player?.stop(); + + @override + Widget? videoWidget( + Key key, + BoxFit fit, + ) => + _controller == null + ? null + : Video( + key: key, + controller: _controller!, + wakelock: true, + fill: Colors.transparent, + fit: fit, + subtitleViewConfiguration: const SubtitleViewConfiguration(visible: false), + controls: NoVideoControls, + ); + + @override + Widget? subtitles( + bool showOverlay, + ) => + _controller != null + ? _VideoSubtitles( + controller: _controller!, + showOverlay: showOverlay, + ) + : null; + + @override + Future setVolume(double volume) async => _player?.setVolume(volume); + + @override + Future loop(bool loop) async { + if (loop && _onCompleted == null) { + _onCompleted = _player?.stream.completed.listen((completed) { + if (completed) { + _player?.play(); + } + }); + } else { + _onCompleted?.cancel(); + } + } +} + +class _VideoSubtitles extends ConsumerStatefulWidget { + final VideoController controller; + final bool showOverlay; + const _VideoSubtitles({ + required this.controller, + this.showOverlay = false, + }); + + @override + ConsumerState createState() => _VideoSubtitlesState(); +} + +class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { + late List subtitle = widget.controller.player.state.subtitle; + StreamSubscription>? 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.shrink()); + } else { + return SubtitleText( + subModel: settings, + padding: padding, + offset: (widget.showOverlay ? 0.5 : settings.verticalOffset), + text: text, + ); + } + } +} diff --git a/lib/wrappers/players/player_states.dart b/lib/wrappers/players/player_states.dart new file mode 100644 index 0000000..a73e4f6 --- /dev/null +++ b/lib/wrappers/players/player_states.dart @@ -0,0 +1,74 @@ +class PlayerState { + bool playing; + bool completed; + Duration position; + Duration duration; + double volume; + double rate; + bool buffering; + Duration buffer; + + PlayerState({ + this.playing = false, + this.completed = false, + this.position = Duration.zero, + this.duration = Duration.zero, + this.volume = 100, + this.rate = 1.0, + this.buffering = true, + this.buffer = Duration.zero, + }); + + PlayerState update({ + bool? playing, + bool? completed, + bool? buffering, + Duration? position, + Duration? duration, + double? volume, + double? rate, + Duration? buffer, + }) { + if (playing != null) this.playing = playing; + if (completed != null) this.completed = completed; + if (buffering != null) this.buffering = buffering; + if (position != null) this.position = position; + if (duration != null) this.duration = duration; + if (volume != null) this.volume = volume; + if (rate != null) this.rate = rate; + if (buffer != null) this.buffer = buffer; + return this; + } +} + +class PlayerStream { + final Stream playing; + final Stream completed; + final Stream position; + final Stream duration; + final Stream volume; + final Stream rate; + final Stream buffering; + final Stream buffer; + + const PlayerStream( + this.playing, + this.completed, + this.position, + this.duration, + this.volume, + this.rate, + this.buffering, + this.buffer, + ); + + void bindToState(PlayerState state) { + playing.listen((value) => state.update(playing: value)); + buffering.listen((value) => state.update(buffering: value)); + position.listen((value) => state.update(position: value)); + duration.listen((value) => state.update(duration: value)); + volume.listen((value) => state.update(volume: value)); + rate.listen((value) => state.update(rate: value)); + buffer.listen((value) => state.update(buffer: value)); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index a6f5cf6..4f69b99 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -22,6 +23,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) dynamic_color_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); + g_autoptr(FlPluginRegistrar) fvp_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FvpPlugin"); + fvp_plugin_register_with_registrar(fvp_registrar); g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index b16442a..ba48f10 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_drop dynamic_color + fvp isar_flutter_libs media_kit_libs_linux media_kit_video diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 57b7b65..21390d6 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import audio_service import audio_session import desktop_drop import dynamic_color +import fvp import isar_flutter_libs import just_audio import local_auth_darwin @@ -32,6 +33,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) + FvpPlugin.register(with: registry.registrar(forPlugin: "FvpPlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 95f5e70..8ea22e7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -746,6 +746,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + fvp: + dependency: "direct main" + description: + name: fvp + sha256: "3dd245cac5dfba36311cbf5834d8f275ba1f3e49a5cdcb4a98e01cb41e9a21d8" + url: "https://pub.dev" + source: hosted + version: "0.28.0" fwfh_cached_network_image: dependency: transitive description: @@ -2016,7 +2024,7 @@ packages: source: hosted version: "2.1.4" video_player: - dependency: transitive + dependency: "direct main" description: name: video_player sha256: "4a8c3492d734f7c39c2588a3206707a05ee80cef52e8c7f3b2078d430c84bc17" diff --git a/pubspec.yaml b/pubspec.yaml index 0cf1d8f..457b7ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,8 @@ dependencies: media_kit_video: ^1.2.4 # For video rendering. media_kit_libs_video: ^1.0.4 # Native video dependencies. audio_service: ^0.18.12 + fvp: ^0.28.0 + video_player: ^2.9.2 # UI Components dynamic_color: ^1.7.0 @@ -158,6 +160,7 @@ flutter: - icons/ - assets/fonts/ - config/ + - assets/mp-font.ttf fonts: - family: Rubik @@ -173,7 +176,3 @@ flutter: style: normal - asset: assets/fonts/opensans/OpenSans-Italic.ttf style: italic - - - family: mp-font - fonts: - - asset: assets/fonts/mp-font.ttf diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b26f116..bf950b5 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -24,6 +25,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("DesktopDropPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); + FvpPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FvpPluginCApi")); IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); LocalAuthPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index fa7bb92..211ea4d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_drop dynamic_color + fvp isar_flutter_libs local_auth_windows media_kit_libs_windows_video