import 'dart:io'; import 'package:flutter/services.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.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 _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 _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 _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 _extractShader(String assetPath) async { try { final shaderDir = await _getShaderDirectory(); final fileName = path.basename(assetPath); final subDir = path.dirname(assetPath); 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> 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> 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> getAnime4KShaders(Anime4KConfig config) async { final shaders = []; final quality = config.quality; final mode = config.mode; 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 _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 importCustomShader(String sourcePath) async { final customDir = await _getCustomShaderDirectory(); final ext = path.extension(sourcePath); final uuid = const Uuid().v4(); 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 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 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> 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 preloadShaders() async { try { await _extractShader(_nvscalerShader); for (final shaderPath in _artcnnShaders.values) { await _extractShader(shaderPath); } 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; } }