From 1fcea7dc5a21a1564cacc57aac5f22455f28f16b Mon Sep 17 00:00:00 2001 From: JenteJan Date: Sun, 7 Jun 2026 22:33:53 +0200 Subject: [PATCH] feat(player): configurable play/pause fade duration The play/pause volume fade was hardcoded to 175ms, which delays the actual mpv pause until the fade completes and makes toggling playback feel sluggish compared to native players like mpc/mpv. Expose the fade length as a 'Play/pause fade duration' setting (0-1000ms) shown beneath the existing fade toggle. A value of 0 (or any duration shorter than one fade step) now skips the fade entirely and hits mpv immediately for an instant, snappy response. Default stays at 175ms to preserve current behaviour. --- lib/l10n/app_en.arb | 4 ++ .../settings/video_player_settings.dart | 1 + .../video_player_settings.freezed.dart | 31 ++++++++++++++- .../settings/video_player_settings.g.dart | 3 ++ .../video_player_settings_provider.dart | 2 + .../settings/player_settings_page.dart | 38 +++++++++++++++++++ lib/wrappers/players/lib_mpv.dart | 13 +++++-- 7 files changed, 86 insertions(+), 6 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d2fa5c6d3..75a495862 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -984,6 +984,10 @@ "@settingsPlayerPlayPauseFadeTitle": {}, "settingsPlayerPlayPauseFadeDesc": "Fade volume in and out when playing or pausing", "@settingsPlayerPlayPauseFadeDesc": {}, + "settingsPlayerPlayPauseFadeDurationTitle": "Play/pause fade duration", + "@settingsPlayerPlayPauseFadeDurationTitle": {}, + "settingsPlayerPlayPauseFadeDurationDesc": "How long the volume fade takes when playing or pausing. Lower values feel snappier; set to 0 for an instant response.", + "@settingsPlayerPlayPauseFadeDurationDesc": {}, "settingsPlayerBufferSizeDesc": "Configure the buffer size for video playback, determining how much data is loaded into the cache.", "@settingsPlayerBufferSizeDesc": {}, "settingsPlayerTitle": "Player", diff --git a/lib/models/settings/video_player_settings.dart b/lib/models/settings/video_player_settings.dart index 4afc33f09..0b3b160a2 100644 --- a/lib/models/settings/video_player_settings.dart +++ b/lib/models/settings/video_player_settings.dart @@ -108,6 +108,7 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { @Default(true) bool enableReplayGain, @Default(ReplayGainVolumeLevel.quiet) ReplayGainVolumeLevel replayGainVolumeLevel, @Default(true) bool enablePlayPauseFade, + @Default(175) int playPauseFadeDurationMs, @Default(true) bool enableCrossfade, @Default(400) int crossfadeDurationMs, }) = _VideoPlayerSettingsModel; diff --git a/lib/models/settings/video_player_settings.freezed.dart b/lib/models/settings/video_player_settings.freezed.dart index 0046a082f..d3c67ecd3 100644 --- a/lib/models/settings/video_player_settings.freezed.dart +++ b/lib/models/settings/video_player_settings.freezed.dart @@ -41,6 +41,7 @@ mixin _$VideoPlayerSettingsModel implements DiagnosticableTreeMixin { bool get enableReplayGain; ReplayGainVolumeLevel get replayGainVolumeLevel; bool get enablePlayPauseFade; + int get playPauseFadeDurationMs; bool get enableCrossfade; int get crossfadeDurationMs; @@ -88,13 +89,15 @@ mixin _$VideoPlayerSettingsModel implements DiagnosticableTreeMixin { ..add(DiagnosticsProperty('enableReplayGain', enableReplayGain)) ..add(DiagnosticsProperty('replayGainVolumeLevel', replayGainVolumeLevel)) ..add(DiagnosticsProperty('enablePlayPauseFade', enablePlayPauseFade)) + ..add( + DiagnosticsProperty('playPauseFadeDurationMs', playPauseFadeDurationMs)) ..add(DiagnosticsProperty('enableCrossfade', enableCrossfade)) ..add(DiagnosticsProperty('crossfadeDurationMs', crossfadeDurationMs)); } @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, enableEdgeGestures: $enableEdgeGestures, reverseEdgeGestures: $reverseEdgeGestures, enablePictureInPicture: $enablePictureInPicture, enableReplayGain: $enableReplayGain, replayGainVolumeLevel: $replayGainVolumeLevel, enablePlayPauseFade: $enablePlayPauseFade, enableCrossfade: $enableCrossfade, crossfadeDurationMs: $crossfadeDurationMs)'; + return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, enableEdgeGestures: $enableEdgeGestures, reverseEdgeGestures: $reverseEdgeGestures, enablePictureInPicture: $enablePictureInPicture, enableReplayGain: $enableReplayGain, replayGainVolumeLevel: $replayGainVolumeLevel, enablePlayPauseFade: $enablePlayPauseFade, playPauseFadeDurationMs: $playPauseFadeDurationMs, enableCrossfade: $enableCrossfade, crossfadeDurationMs: $crossfadeDurationMs)'; } } @@ -132,6 +135,7 @@ abstract mixin class $VideoPlayerSettingsModelCopyWith<$Res> { bool enableReplayGain, ReplayGainVolumeLevel replayGainVolumeLevel, bool enablePlayPauseFade, + int playPauseFadeDurationMs, bool enableCrossfade, int crossfadeDurationMs}); } @@ -176,6 +180,7 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res> Object? enableReplayGain = null, Object? replayGainVolumeLevel = null, Object? enablePlayPauseFade = null, + Object? playPauseFadeDurationMs = null, Object? enableCrossfade = null, Object? crossfadeDurationMs = null, }) { @@ -288,6 +293,10 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res> ? _self.enablePlayPauseFade : enablePlayPauseFade // ignore: cast_nullable_to_non_nullable as bool, + playPauseFadeDurationMs: null == playPauseFadeDurationMs + ? _self.playPauseFadeDurationMs + : playPauseFadeDurationMs // ignore: cast_nullable_to_non_nullable + as int, enableCrossfade: null == enableCrossfade ? _self.enableCrossfade : enableCrossfade // ignore: cast_nullable_to_non_nullable @@ -421,6 +430,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { bool enableReplayGain, ReplayGainVolumeLevel replayGainVolumeLevel, bool enablePlayPauseFade, + int playPauseFadeDurationMs, bool enableCrossfade, int crossfadeDurationMs)? $default, { @@ -457,6 +467,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { _that.enableReplayGain, _that.replayGainVolumeLevel, _that.enablePlayPauseFade, + _that.playPauseFadeDurationMs, _that.enableCrossfade, _that.crossfadeDurationMs); case _: @@ -507,6 +518,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { bool enableReplayGain, ReplayGainVolumeLevel replayGainVolumeLevel, bool enablePlayPauseFade, + int playPauseFadeDurationMs, bool enableCrossfade, int crossfadeDurationMs) $default, @@ -542,6 +554,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { _that.enableReplayGain, _that.replayGainVolumeLevel, _that.enablePlayPauseFade, + _that.playPauseFadeDurationMs, _that.enableCrossfade, _that.crossfadeDurationMs); case _: @@ -591,6 +604,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { bool enableReplayGain, ReplayGainVolumeLevel replayGainVolumeLevel, bool enablePlayPauseFade, + int playPauseFadeDurationMs, bool enableCrossfade, int crossfadeDurationMs)? $default, @@ -626,6 +640,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { _that.enableReplayGain, _that.replayGainVolumeLevel, _that.enablePlayPauseFade, + _that.playPauseFadeDurationMs, _that.enableCrossfade, _that.crossfadeDurationMs); case _: @@ -667,6 +682,7 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel this.enableReplayGain = true, this.replayGainVolumeLevel = ReplayGainVolumeLevel.quiet, this.enablePlayPauseFade = true, + this.playPauseFadeDurationMs = 175, this.enableCrossfade = true, this.crossfadeDurationMs = 400}) : _allowedOrientations = allowedOrientations, @@ -777,6 +793,9 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel final bool enablePlayPauseFade; @override @JsonKey() + final int playPauseFadeDurationMs; + @override + @JsonKey() final bool enableCrossfade; @override @JsonKey() @@ -831,13 +850,15 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel ..add(DiagnosticsProperty('enableReplayGain', enableReplayGain)) ..add(DiagnosticsProperty('replayGainVolumeLevel', replayGainVolumeLevel)) ..add(DiagnosticsProperty('enablePlayPauseFade', enablePlayPauseFade)) + ..add( + DiagnosticsProperty('playPauseFadeDurationMs', playPauseFadeDurationMs)) ..add(DiagnosticsProperty('enableCrossfade', enableCrossfade)) ..add(DiagnosticsProperty('crossfadeDurationMs', crossfadeDurationMs)); } @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, enableEdgeGestures: $enableEdgeGestures, reverseEdgeGestures: $reverseEdgeGestures, enablePictureInPicture: $enablePictureInPicture, enableReplayGain: $enableReplayGain, replayGainVolumeLevel: $replayGainVolumeLevel, enablePlayPauseFade: $enablePlayPauseFade, enableCrossfade: $enableCrossfade, crossfadeDurationMs: $crossfadeDurationMs)'; + return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, enableEdgeGestures: $enableEdgeGestures, reverseEdgeGestures: $reverseEdgeGestures, enablePictureInPicture: $enablePictureInPicture, enableReplayGain: $enableReplayGain, replayGainVolumeLevel: $replayGainVolumeLevel, enablePlayPauseFade: $enablePlayPauseFade, playPauseFadeDurationMs: $playPauseFadeDurationMs, enableCrossfade: $enableCrossfade, crossfadeDurationMs: $crossfadeDurationMs)'; } } @@ -877,6 +898,7 @@ abstract mixin class _$VideoPlayerSettingsModelCopyWith<$Res> bool enableReplayGain, ReplayGainVolumeLevel replayGainVolumeLevel, bool enablePlayPauseFade, + int playPauseFadeDurationMs, bool enableCrossfade, int crossfadeDurationMs}); } @@ -921,6 +943,7 @@ class __$VideoPlayerSettingsModelCopyWithImpl<$Res> Object? enableReplayGain = null, Object? replayGainVolumeLevel = null, Object? enablePlayPauseFade = null, + Object? playPauseFadeDurationMs = null, Object? enableCrossfade = null, Object? crossfadeDurationMs = null, }) { @@ -1033,6 +1056,10 @@ class __$VideoPlayerSettingsModelCopyWithImpl<$Res> ? _self.enablePlayPauseFade : enablePlayPauseFade // ignore: cast_nullable_to_non_nullable as bool, + playPauseFadeDurationMs: null == playPauseFadeDurationMs + ? _self.playPauseFadeDurationMs + : playPauseFadeDurationMs // ignore: cast_nullable_to_non_nullable + as int, enableCrossfade: null == enableCrossfade ? _self.enableCrossfade : enableCrossfade // ignore: cast_nullable_to_non_nullable diff --git a/lib/models/settings/video_player_settings.g.dart b/lib/models/settings/video_player_settings.g.dart index c6e599658..e7b8ecab5 100644 --- a/lib/models/settings/video_player_settings.g.dart +++ b/lib/models/settings/video_player_settings.g.dart @@ -60,6 +60,8 @@ _VideoPlayerSettingsModel _$VideoPlayerSettingsModelFromJson( _$ReplayGainVolumeLevelEnumMap, json['replayGainVolumeLevel']) ?? ReplayGainVolumeLevel.quiet, enablePlayPauseFade: json['enablePlayPauseFade'] as bool? ?? true, + playPauseFadeDurationMs: + (json['playPauseFadeDurationMs'] as num?)?.toInt() ?? 175, enableCrossfade: json['enableCrossfade'] as bool? ?? true, crossfadeDurationMs: (json['crossfadeDurationMs'] as num?)?.toInt() ?? 400, @@ -100,6 +102,7 @@ Map _$VideoPlayerSettingsModelToJson( 'replayGainVolumeLevel': _$ReplayGainVolumeLevelEnumMap[instance.replayGainVolumeLevel]!, 'enablePlayPauseFade': instance.enablePlayPauseFade, + 'playPauseFadeDurationMs': instance.playPauseFadeDurationMs, 'enableCrossfade': instance.enableCrossfade, 'crossfadeDurationMs': instance.crossfadeDurationMs, }; diff --git a/lib/providers/settings/video_player_settings_provider.dart b/lib/providers/settings/video_player_settings_provider.dart index ce883c021..a4d1f6b3b 100644 --- a/lib/providers/settings/video_player_settings_provider.dart +++ b/lib/providers/settings/video_player_settings_provider.dart @@ -187,6 +187,8 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier state = state.copyWith(enablePlayPauseFade: value); + void setPlayPauseFadeDurationMs(int value) => state = state.copyWith(playPauseFadeDurationMs: value.clamp(0, 1000)); + void setReplayGainVolumeLevel(ReplayGainVolumeLevel value) => state = state.copyWith(replayGainVolumeLevel: value); void setEnableCrossfade(bool value) { diff --git a/lib/screens/settings/player_settings_page.dart b/lib/screens/settings/player_settings_page.dart index a710a9fbb..16f93e57d 100644 --- a/lib/screens/settings/player_settings_page.dart +++ b/lib/screens/settings/player_settings_page.dart @@ -437,6 +437,44 @@ class _PlayerSettingsPageState extends ConsumerState { onChanged: (value) => provider.setEnablePlayPauseFade(value), ), ), + if (currentPlayer == PlayerOptions.libMPV && videoSettings.enablePlayPauseFade) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.localized.settingsPlayerPlayPauseFadeDurationTitle, + style: Theme.of(context).textTheme.titleLarge, + ), + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + context.localized.settingsPlayerPlayPauseFadeDurationDesc, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + Row( + children: [ + Expanded( + child: FladderSlider( + min: 0, + max: 1000, + value: videoSettings.playPauseFadeDurationMs.toDouble(), + divisions: 40, + onChanged: (value) => provider.setPlayPauseFadeDurationMs(value.round()), + ), + ), + const SizedBox(width: 12), + Text( + '${videoSettings.playPauseFadeDurationMs} ms', + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + ], + ), + ), if (currentPlayer == PlayerOptions.libMPV) SettingsListTile( label: Text(context.localized.settingsPlayerBufferSizeTitle), diff --git a/lib/wrappers/players/lib_mpv.dart b/lib/wrappers/players/lib_mpv.dart index e17a8386a..379cf18ff 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -46,7 +46,7 @@ class LibMPV extends BasePlayer { double _preferredVolume = 100; int _crossfadeGeneration = 0; Timer? _fadeTimer; - Duration get playPauseFadeDuration => const Duration(milliseconds: 175); + Duration get playPauseFadeDuration => Duration(milliseconds: _settings.playPauseFadeDurationMs); @override Future init(VideoPlayerSettingsModel settings) async { @@ -372,8 +372,15 @@ class LibMPV extends BasePlayer { _fadeTimer?.cancel(); - if (!_settings.enablePlayPauseFade) { + const stepMs = 16; + final steps = playPauseFadeDuration.inMilliseconds ~/ stepMs; + + // Skip the fade entirely when disabled or the configured duration is too + // short to produce at least one step. This makes play/pause hit mpv + // immediately for a snappy, mpc-like response. + if (!_settings.enablePlayPauseFade || steps <= 0) { if (fadingIn) { + player.setVolume(_preferredVolume); player.play(); } else { player.pause(); @@ -381,8 +388,6 @@ class LibMPV extends BasePlayer { return; } - const stepMs = 16; - final steps = playPauseFadeDuration.inMilliseconds ~/ stepMs; final stepSize = _preferredVolume / steps; if (fadingIn) player.play();