Shaders: Anime4K presets in-session and in Settings
Vendored Anime4K v4.0.1 GLSL (MIT), extracted to app support once per version. Presets A/B/C/A+A/B+B/C+A in HQ and Fast per Anime4K's mpv templates, plus custom files. Shader button in the video controls (works in fullscreen), Ctrl+1..6 / Ctrl+0, live apply with read-back, failures fall back to Off with a toast. Render mode detected from mpv's GL log; warns when on the software path. Docs include the Jellyfin client section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4814bfb561
commit
fb2927c094
28 files changed
+6516
-53
No files matched your search
@@ -6,6 +6,8 @@ import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
import 'package:signals/signals_flutter.dart';
|
||||
|
||||
import '../shaders/shader_assets.dart';
|
||||
import '../shaders/shader_presets.dart';
|
||||
import '../state/settings.dart';
|
||||
import '../sync/state_diff.dart';
|
||||
import '../sync/user_seek.dart';
|
||||
@@ -32,6 +34,11 @@ class PlaybackController {
|
||||
Timer? _watchTimer;
|
||||
bool _probing = false;
|
||||
final FrameWatchdog _watchdog = FrameWatchdog();
|
||||
StreamSubscription<PlayerLog>? _logSub;
|
||||
// `glsl-shaders` as last set on the current player ('' = none).
|
||||
String _shaders = '';
|
||||
Future<void> _shaderOp = Future.value();
|
||||
int _noticeId = 0;
|
||||
|
||||
/// Bumped for every new player; the video view keys itself on it so a
|
||||
/// recreated player gets a fresh `Video` widget.
|
||||
@@ -53,6 +60,14 @@ class PlaybackController {
|
||||
/// Seeks from the video controls (seek bar) go here instead of to mpv.
|
||||
void Function(Duration position)? onUserSeek;
|
||||
|
||||
/// Latest shader message for the user (the session screen toasts it).
|
||||
final shaderNotice = signal<ShaderNotice?>(null);
|
||||
|
||||
/// How media_kit renders the current player's video (shaders need
|
||||
/// [RenderMode.gpu]); [lastRenderMode] keeps it after leaving.
|
||||
final renderMode = signal(RenderMode.unknown);
|
||||
static final lastRenderMode = signal(RenderMode.unknown);
|
||||
|
||||
/// Pinned mpv properties whose read-back differs from what was set
|
||||
/// (mpv ignores invalid values silently), as `name: wanted -> got`.
|
||||
final List<String> pinMismatches = [];
|
||||
@@ -67,10 +82,12 @@ class PlaybackController {
|
||||
final hwdec = _hwdecOverride;
|
||||
// media_kit's video output sets hwdec itself on attach ('auto' unless
|
||||
// configured), which used to override SYNC_RELAY_HWDEC.
|
||||
return _video = VideoController(
|
||||
final video = _video = VideoController(
|
||||
player,
|
||||
configuration: VideoControllerConfiguration(hwdec: hwdec),
|
||||
);
|
||||
unawaited(_detectRenderMode(player, video));
|
||||
return video;
|
||||
}
|
||||
|
||||
// SYNC_RELAY_HWDEC wins over the Settings choice; read per player, so a
|
||||
@@ -88,9 +105,16 @@ class PlaybackController {
|
||||
bufferSize: maxBytes,
|
||||
libass: true,
|
||||
vo: Platform.environment['SYNC_RELAY_VO'],
|
||||
// Verbose: the render-mode probe needs mpv's GL init lines.
|
||||
logLevel: MPVLogLevel.v,
|
||||
))
|
||||
..onUserSeek = (d) => onUserSeek?.call(d);
|
||||
_shaders = '';
|
||||
renderMode.value = RenderMode.unknown;
|
||||
shaderNotice.value = null;
|
||||
_logSub = _player!.stream.log.listen(_onLog);
|
||||
await _pinBufferProps();
|
||||
await applyShaders();
|
||||
_watchdog.reset();
|
||||
_watchTimer = Timer.periodic(const Duration(seconds: 1), (_) => _watch());
|
||||
generation.value++;
|
||||
@@ -139,6 +163,132 @@ class PlaybackController {
|
||||
'reconnect_streamed=1,reconnect_on_http_error=1,reconnect_delay_max=4');
|
||||
}
|
||||
|
||||
/// Puts the saved shader choice (Settings, Ctrl+0..6, shader button) on
|
||||
/// the current player, live. A missing file or a value mpv won't take
|
||||
/// turns shaders off and says why in [shaderNotice]; [announce] also
|
||||
/// reports success ("Shaders: Mode A (HQ)").
|
||||
Future<void> applyShaders({bool announce = false}) =>
|
||||
_shaderOp = _shaderOp.then((_) => _applyShaders(announce));
|
||||
|
||||
Future<void> _applyShaders(bool announce) async {
|
||||
final p = _player;
|
||||
if (p == null) return;
|
||||
final s = AppSettings.instance;
|
||||
final preset = s.shaderPreset.peek();
|
||||
final label = shaderLabel(preset, s.shaderQuality.peek());
|
||||
String? problem;
|
||||
var paths = const <String>[];
|
||||
try {
|
||||
paths = await shaderPathsFor(
|
||||
preset, s.shaderQuality.peek(), s.customShaders.peek());
|
||||
if (preset == ShaderPreset.custom && paths.isEmpty) {
|
||||
problem = 'no custom shader files picked';
|
||||
} else {
|
||||
problem = shaderPathsProblem(paths);
|
||||
}
|
||||
} catch (e) {
|
||||
problem = 'could not unpack the shaders ($e)';
|
||||
}
|
||||
if (!identical(p, _player)) return;
|
||||
if (problem != null) paths = const [];
|
||||
final value = joinShaderPaths(paths);
|
||||
if (value != _shaders || problem != null) {
|
||||
final got = await setShaderValue(value);
|
||||
if (!identical(p, _player)) return;
|
||||
if (problem == null && got != value) {
|
||||
problem = 'mpv did not take them';
|
||||
await setShaderValue('');
|
||||
}
|
||||
}
|
||||
if (problem != null) {
|
||||
_turnShadersOff('$label failed: $problem — turned off');
|
||||
} else if (announce) {
|
||||
_notice('Shaders: $label${_softwareHint(paths)}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets mpv `glsl-shaders` on the current player and returns the
|
||||
/// read-back (null on error).
|
||||
@visibleForTesting
|
||||
Future<String?> setShaderValue(String value) async {
|
||||
try {
|
||||
final dynamic platform = player.platform;
|
||||
await platform?.setProperty('glsl-shaders', value);
|
||||
final got = '${await platform?.getProperty('glsl-shaders')}';
|
||||
_shaders = got;
|
||||
if (got != value) debugPrint('glsl-shaders=$value not applied: "$got"');
|
||||
return got;
|
||||
} catch (e, st) {
|
||||
debugPrint('glsl-shaders=$value failed: $e\n$st');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _turnShadersOff(String message) {
|
||||
AppSettings.instance.setShaderPreset(ShaderPreset.off);
|
||||
_notice(message);
|
||||
}
|
||||
|
||||
void _notice(String text) {
|
||||
shaderNotice.value = ShaderNotice(++_noticeId, text);
|
||||
}
|
||||
|
||||
String _softwareHint(List<String> paths) => paths.isEmpty
|
||||
? ''
|
||||
: switch (renderMode.peek()) {
|
||||
RenderMode.software => ' (no effect: video uses software rendering)',
|
||||
RenderMode.softwareGl => ' (likely slow: OpenGL runs on the CPU)',
|
||||
_ => '',
|
||||
};
|
||||
|
||||
// mpv logs from the render context: GL init lines mean GPU rendering;
|
||||
// an error there while shaders are on is a shader that failed to load
|
||||
// or compile (mpv then renders without it).
|
||||
void _onLog(PlayerLog log) {
|
||||
if (log.prefix != 'libmpv_render') return;
|
||||
if (log.text.contains('suspected software renderer')) {
|
||||
_setRenderMode(RenderMode.softwareGl);
|
||||
} else if (log.text.startsWith('GL_VERSION') &&
|
||||
renderMode.peek() == RenderMode.unknown) {
|
||||
_setRenderMode(RenderMode.gpu);
|
||||
} else if ((log.level == 'error' || log.level == 'fatal') &&
|
||||
_shaders.isNotEmpty) {
|
||||
debugPrint('shader error: ${log.text}');
|
||||
final p = _player;
|
||||
_shaders = '';
|
||||
_shaderOp = _shaderOp.then((_) async {
|
||||
if (!identical(p, _player)) return;
|
||||
await setShaderValue('');
|
||||
_turnShadersOff('Shaders failed in mpv (${log.text}) — turned off');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _setRenderMode(RenderMode mode) {
|
||||
renderMode.value = mode;
|
||||
lastRenderMode.value = mode;
|
||||
}
|
||||
|
||||
// media_kit's Linux/Windows video output creates mpv's render context
|
||||
// while the controller is created: OpenGL when it can, else the
|
||||
// software renderer, which never logs GL init (and ignores shaders).
|
||||
Future<void> _detectRenderMode(Player p, VideoController video) async {
|
||||
try {
|
||||
await video.platform.future;
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 1500));
|
||||
if (!identical(p, _player) || renderMode.peek() != RenderMode.unknown) {
|
||||
return;
|
||||
}
|
||||
_setRenderMode(RenderMode.software);
|
||||
debugPrint('video output: software rendering (no GL init logged)');
|
||||
if (_shaders.isNotEmpty) {
|
||||
_notice('Shaders have no effect: video uses software rendering');
|
||||
}
|
||||
}
|
||||
|
||||
/// Fingerprint of the decoder's current frame (mpv `screenshot-raw`,
|
||||
/// off-thread); null without video.
|
||||
Future<int?> _sourceFrame(Player p) async {
|
||||
@@ -267,6 +417,8 @@ class PlaybackController {
|
||||
if (p == null) return;
|
||||
_watchTimer?.cancel();
|
||||
_watchTimer = null;
|
||||
await _logSub?.cancel();
|
||||
_logSub = null;
|
||||
_player = null;
|
||||
_video = null;
|
||||
alive.value = false;
|
||||
@@ -300,6 +452,22 @@ class PlaybackController {
|
||||
}
|
||||
}
|
||||
|
||||
enum RenderMode {
|
||||
unknown,
|
||||
/// OpenGL on a real GPU: shaders work.
|
||||
gpu,
|
||||
/// OpenGL on a software rasterizer (llvmpipe): shaders crawl.
|
||||
softwareGl,
|
||||
/// media_kit's software fallback: shaders are ignored.
|
||||
software,
|
||||
}
|
||||
|
||||
class ShaderNotice {
|
||||
final int id;
|
||||
final String text;
|
||||
const ShaderNotice(this.id, this.text);
|
||||
}
|
||||
|
||||
/// mpv echoes numbers in its own format (`150` reads back `150.000000`).
|
||||
bool pinMatches(String wanted, String got) {
|
||||
if (wanted == got) return true;
|
||||
|
||||
Reference in new issue
Block a user