288 lines
11 KiB
Dart
288 lines
11 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/services.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
import '../models/shader_preset.dart';
|
|
import '../utils/app_logger.dart';
|
|
|
|
/// Utility class for loading GLSL shader assets for MPV video enhancement.
|
|
///
|
|
/// Extracts shader files from Flutter assets to the app's cache directory
|
|
/// where MPV can access them at runtime.
|
|
class ShaderAssetLoader {
|
|
static const String _shaderAssetBase = 'assets/shaders';
|
|
static String? _cachedShaderDir;
|
|
|
|
/// NVScaler shader file
|
|
static const String _nvscalerShader = 'nvscaler/NVScaler.glsl';
|
|
|
|
/// ArtCNN shader files organized by preset model and variant.
|
|
static const Map<String, String> _artcnnShaders = {
|
|
'c4f16_neutral': 'artcnn/ArtCNN_C4F16.glsl',
|
|
'c4f16_dn': 'artcnn/ArtCNN_C4F16_DN.glsl',
|
|
'c4f16_ds': 'artcnn/ArtCNN_C4F16_DS.glsl',
|
|
'c4f32_neutral': 'artcnn/ArtCNN_C4F32.glsl',
|
|
'c4f32_dn': 'artcnn/ArtCNN_C4F32_DN.glsl',
|
|
'c4f32_ds': 'artcnn/ArtCNN_C4F32_DS.glsl',
|
|
};
|
|
|
|
/// Anime4K shader files organized by function
|
|
static const Map<String, String> _anime4kShaders = {
|
|
'clamp': 'anime4k/Anime4K_Clamp_Highlights.glsl',
|
|
'restore_m': 'anime4k/Anime4K_Restore_CNN_M.glsl',
|
|
'restore_vl': 'anime4k/Anime4K_Restore_CNN_VL.glsl',
|
|
'restore_ul': 'anime4k/Anime4K_Restore_CNN_UL.glsl',
|
|
'upscale_m': 'anime4k/Anime4K_Upscale_CNN_x2_M.glsl',
|
|
'upscale_vl': 'anime4k/Anime4K_Upscale_CNN_x2_VL.glsl',
|
|
'upscale_ul': 'anime4k/Anime4K_Upscale_CNN_x2_UL.glsl',
|
|
'downscale': 'anime4k/Anime4K_AutoDownscalePre_x2.glsl',
|
|
'downscale_post': 'anime4k/Anime4K_AutoDownscalePre_x4.glsl',
|
|
};
|
|
|
|
/// Get the shader cache directory path, creating it if necessary.
|
|
static Future<String> _getShaderDirectory() async {
|
|
if (_cachedShaderDir != null) return _cachedShaderDir!;
|
|
|
|
final cacheDir = await getTemporaryDirectory();
|
|
final shaderDir = Directory(path.join(cacheDir.path, 'shaders'));
|
|
|
|
if (!await shaderDir.exists()) {
|
|
await shaderDir.create(recursive: true);
|
|
}
|
|
|
|
_cachedShaderDir = shaderDir.path;
|
|
return shaderDir.path;
|
|
}
|
|
|
|
/// Extract a single shader file from assets to the cache directory.
|
|
/// Returns the absolute file path of the extracted shader.
|
|
static Future<String?> _extractShader(String assetPath) async {
|
|
try {
|
|
final shaderDir = await _getShaderDirectory();
|
|
final fileName = path.basename(assetPath);
|
|
final subDir = path.dirname(assetPath);
|
|
|
|
// Create subdirectory if needed
|
|
final targetDir = Directory(path.join(shaderDir, subDir));
|
|
if (!await targetDir.exists()) {
|
|
await targetDir.create(recursive: true);
|
|
}
|
|
|
|
final targetFile = File(path.join(targetDir.path, fileName));
|
|
|
|
// Only extract if not already cached
|
|
if (!await targetFile.exists()) {
|
|
final fullAssetPath = '$_shaderAssetBase/$assetPath';
|
|
final data = await rootBundle.load(fullAssetPath);
|
|
await targetFile.writeAsBytes(data.buffer.asUint8List());
|
|
}
|
|
|
|
return targetFile.path;
|
|
} catch (e, st) {
|
|
appLogger.w('Failed to extract shader $assetPath', error: e, stackTrace: st);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Get the shader file paths for NVScaler preset.
|
|
/// Returns a list containing the single NVScaler shader path.
|
|
static Future<List<String>> getNVScalerShaders() async {
|
|
final shaderPath = await _extractShader(_nvscalerShader);
|
|
if (shaderPath == null) return [];
|
|
return [shaderPath];
|
|
}
|
|
|
|
/// Get the shader file path for an ArtCNN preset.
|
|
/// Returns a list containing exactly one ArtCNN shader path.
|
|
static Future<List<String>> getArtCNNShaders(ArtCNNConfig config) async {
|
|
final variantId = switch (config.variant) {
|
|
ArtCNNVariant.neutral => 'neutral',
|
|
ArtCNNVariant.denoise => 'dn',
|
|
ArtCNNVariant.denoiseSharpen => 'ds',
|
|
};
|
|
final shaderPath = await _extractShader(_artcnnShaders['${config.model.name}_$variantId']!);
|
|
if (shaderPath == null) return [];
|
|
return [shaderPath];
|
|
}
|
|
|
|
/// Get the shader file paths for an Anime4K preset.
|
|
/// Returns a list of shader paths in the correct order for MPV.
|
|
static Future<List<String>> getAnime4KShaders(Anime4KConfig config) async {
|
|
final shaders = <String>[];
|
|
final quality = config.quality;
|
|
final mode = config.mode;
|
|
|
|
// Get quality-specific shader variants
|
|
String restoreVariant;
|
|
String upscaleVariant;
|
|
|
|
switch (quality) {
|
|
case Anime4KQuality.fast:
|
|
restoreVariant = 'restore_m';
|
|
upscaleVariant = 'upscale_m';
|
|
break;
|
|
case Anime4KQuality.hq:
|
|
restoreVariant = 'restore_vl';
|
|
upscaleVariant = 'upscale_vl';
|
|
break;
|
|
}
|
|
|
|
// Build shader chain based on mode
|
|
// All modes start with Clamp
|
|
final clampPath = await _extractShader(_anime4kShaders['clamp']!);
|
|
if (clampPath != null) shaders.add(clampPath);
|
|
|
|
switch (mode) {
|
|
case Anime4KMode.modeA:
|
|
// A: Clamp + Restore
|
|
final restorePath = await _extractShader(_anime4kShaders[restoreVariant]!);
|
|
if (restorePath != null) shaders.add(restorePath);
|
|
break;
|
|
|
|
case Anime4KMode.modeB:
|
|
// B: Clamp + Restore + Upscale + Downscale
|
|
final restorePath = await _extractShader(_anime4kShaders[restoreVariant]!);
|
|
if (restorePath != null) shaders.add(restorePath);
|
|
final upscalePath = await _extractShader(_anime4kShaders[upscaleVariant]!);
|
|
if (upscalePath != null) shaders.add(upscalePath);
|
|
final downscalePath = await _extractShader(_anime4kShaders['downscale']!);
|
|
if (downscalePath != null) shaders.add(downscalePath);
|
|
break;
|
|
|
|
case Anime4KMode.modeC:
|
|
// C: Clamp + Upscale + Downscale
|
|
final upscalePath = await _extractShader(_anime4kShaders[upscaleVariant]!);
|
|
if (upscalePath != null) shaders.add(upscalePath);
|
|
final downscalePath = await _extractShader(_anime4kShaders['downscale']!);
|
|
if (downscalePath != null) shaders.add(downscalePath);
|
|
break;
|
|
|
|
case Anime4KMode.modeAA:
|
|
// A+A: Clamp + Restore + Restore
|
|
final restorePath = await _extractShader(_anime4kShaders[restoreVariant]!);
|
|
if (restorePath != null) {
|
|
shaders.add(restorePath);
|
|
shaders.add(restorePath); // Second restore pass
|
|
}
|
|
break;
|
|
|
|
case Anime4KMode.modeBB:
|
|
// B+B: Clamp + Restore + Restore + Upscale + Downscale
|
|
final restorePath = await _extractShader(_anime4kShaders[restoreVariant]!);
|
|
if (restorePath != null) {
|
|
shaders.add(restorePath);
|
|
shaders.add(restorePath); // Second restore pass
|
|
}
|
|
final upscalePath = await _extractShader(_anime4kShaders[upscaleVariant]!);
|
|
if (upscalePath != null) shaders.add(upscalePath);
|
|
final downscalePath = await _extractShader(_anime4kShaders['downscale']!);
|
|
if (downscalePath != null) shaders.add(downscalePath);
|
|
break;
|
|
|
|
case Anime4KMode.modeCA:
|
|
// C+A: Clamp + Upscale + Restore + Downscale
|
|
final upscalePath = await _extractShader(_anime4kShaders[upscaleVariant]!);
|
|
if (upscalePath != null) shaders.add(upscalePath);
|
|
final restorePath = await _extractShader(_anime4kShaders[restoreVariant]!);
|
|
if (restorePath != null) shaders.add(restorePath);
|
|
final downscalePath = await _extractShader(_anime4kShaders['downscale']!);
|
|
if (downscalePath != null) shaders.add(downscalePath);
|
|
break;
|
|
}
|
|
|
|
return shaders;
|
|
}
|
|
|
|
/// Get the custom shader directory path, creating it if necessary.
|
|
/// Uses app support directory (persistent) rather than temp/cache.
|
|
static Future<String> _getCustomShaderDirectory() async {
|
|
final supportDir = await getApplicationSupportDirectory();
|
|
final customDir = Directory(path.join(supportDir.path, 'custom_shaders'));
|
|
|
|
if (!await customDir.exists()) {
|
|
await customDir.create(recursive: true);
|
|
}
|
|
|
|
return customDir.path;
|
|
}
|
|
|
|
/// Import a custom shader file into the custom shaders directory.
|
|
/// Returns the stored file name (UUID-based to avoid collisions).
|
|
static Future<String> importCustomShader(String sourcePath) async {
|
|
final customDir = await _getCustomShaderDirectory();
|
|
final ext = path.extension(sourcePath);
|
|
final uuid = DateTime.now().millisecondsSinceEpoch.toRadixString(36);
|
|
final storedName = '$uuid$ext';
|
|
final targetFile = File(path.join(customDir, storedName));
|
|
|
|
await File(sourcePath).copy(targetFile.path);
|
|
return storedName;
|
|
}
|
|
|
|
/// Delete a custom shader file from the custom shaders directory.
|
|
static Future<void> deleteCustomShader(String fileName) async {
|
|
final customDir = await _getCustomShaderDirectory();
|
|
final file = File(path.join(customDir, fileName));
|
|
if (await file.exists()) {
|
|
await file.delete();
|
|
}
|
|
}
|
|
|
|
/// Get the absolute path for a custom shader file.
|
|
static Future<String> getCustomShaderPath(String fileName) async {
|
|
final customDir = await _getCustomShaderDirectory();
|
|
return path.join(customDir, fileName);
|
|
}
|
|
|
|
/// Get shader paths for a given preset.
|
|
/// Returns an empty list for ShaderPresetType.none.
|
|
static Future<List<String>> getShadersForPreset(ShaderPreset preset) async {
|
|
switch (preset.type) {
|
|
case ShaderPresetType.none:
|
|
return [];
|
|
case ShaderPresetType.nvscaler:
|
|
return getNVScalerShaders();
|
|
case ShaderPresetType.artcnn:
|
|
if (preset.artcnnConfig == null) return [];
|
|
return getArtCNNShaders(preset.artcnnConfig!);
|
|
case ShaderPresetType.anime4k:
|
|
if (preset.anime4kConfig == null) return [];
|
|
return getAnime4KShaders(preset.anime4kConfig!);
|
|
case ShaderPresetType.custom:
|
|
if (preset.fileName == null) return [];
|
|
final shaderPath = await getCustomShaderPath(preset.fileName!);
|
|
if (!await File(shaderPath).exists()) return [];
|
|
return [shaderPath];
|
|
}
|
|
}
|
|
|
|
/// Pre-extract all shader files to the cache.
|
|
/// Call this at startup to avoid extraction delay during playback.
|
|
static Future<void> preloadShaders() async {
|
|
try {
|
|
// Extract NVScaler
|
|
await _extractShader(_nvscalerShader);
|
|
|
|
// Extract all ArtCNN shaders
|
|
for (final shaderPath in _artcnnShaders.values) {
|
|
await _extractShader(shaderPath);
|
|
}
|
|
|
|
// Extract all Anime4K shaders
|
|
for (final shaderPath in _anime4kShaders.values) {
|
|
await _extractShader(shaderPath);
|
|
}
|
|
} catch (e, st) {
|
|
appLogger.w('Failed to preload shaders', error: e, stackTrace: st);
|
|
}
|
|
}
|
|
|
|
/// Clear cached shader directory reference.
|
|
/// Call when clearing app cache.
|
|
static void clearCache() {
|
|
_cachedShaderDir = null;
|
|
}
|
|
}
|