Files
plezy/lib/services/shader_service.dart
T

133 lines
4.2 KiB
Dart

import 'dart:io';
import '../models/shader_preset.dart';
import '../mpv/player/player.dart';
import '../utils/app_logger.dart';
import 'ambient_lighting_service.dart';
import 'shader_asset_loader.dart';
/// Service for applying GLSL shaders to the MPV video player.
///
/// Handles shader chain building, HDR detection, and runtime switching.
/// When ambient lighting is active, the ambient lighting shader is always appended last
/// in the chain after any upscaling/processing shaders.
class ShaderService {
final Player _player;
ShaderPreset _currentPreset = ShaderPreset.none;
/// Reference to ambient lighting service for re-appending its shader after chain rebuilds.
AmbientLightingService? ambientLightingService;
ShaderService(this._player);
ShaderPreset get currentPreset => _currentPreset;
static bool get isPlatformSupported => !Platform.isIOS && !Platform.isMacOS;
/// Check if the player is MPV (shaders are MPV-only)
bool get isSupported => _player.playerType == 'mpv' && isPlatformSupported;
/// Apply a shader preset to the video player.
///
/// For NVScaler with auto-HDR skip enabled, will check video colorspace
/// and skip shader application for HDR content.
Future<void> applyPreset(ShaderPreset preset) async {
if (!isSupported) {
appLogger.d('ShaderService: Shaders not supported on ${_player.playerType}');
return;
}
try {
if (preset.type == ShaderPresetType.nvscaler && preset.nvscalerConfig?.autoHdrSkip == true) {
final isHdr = await _isHdrContent();
if (isHdr) {
appLogger.d('ShaderService: Skipping NVScaler on HDR content');
await _clearShaders();
_currentPreset = ShaderPreset.none;
await _reappendAmbientLighting();
return;
}
}
final shaderPaths = await ShaderAssetLoader.getShadersForPreset(preset);
if (shaderPaths.isEmpty) {
// No shaders - clear any existing ones
await _clearShaders();
_currentPreset = preset;
await _reappendAmbientLighting();
return;
}
await _clearShaders();
for (final shaderPath in shaderPaths) {
await _player.command(['change-list', 'glsl-shaders', 'append', shaderPath]);
}
_currentPreset = preset;
// Re-append ambient lighting shader at end of chain
await _reappendAmbientLighting();
appLogger.d('ShaderService: Applied ${preset.name} with ${shaderPaths.length} shaders');
} catch (e, st) {
appLogger.w('ShaderService: Failed to apply preset', error: e, stackTrace: st);
// Don't rethrow - shader failure shouldn't stop playback
}
}
Future<void> _clearShaders() async {
try {
await _player.command(['change-list', 'glsl-shaders', 'clr', '']);
} catch (e, st) {
appLogger.w('ShaderService: Failed to clear shaders', error: e, stackTrace: st);
}
}
/// Re-append the ambient lighting shader if it's active.
/// Called after shader chain rebuilds to keep ambient lighting last.
Future<void> _reappendAmbientLighting() async {
final service = ambientLightingService;
if (service == null || !service.isEnabled) return;
try {
await service.reappendShader();
} catch (e, st) {
appLogger.w('ShaderService: Failed to re-append ambient lighting', error: e, stackTrace: st);
}
}
/// Check if the current video content is HDR.
Future<bool> _isHdrContent() async {
try {
// Check video color matrix for BT.2020 (HDR indicator)
final colormatrix = await _player.getProperty('video-params/colormatrix');
if (colormatrix?.contains('bt.2020') == true) {
return true;
}
// Also check color primaries
final primaries = await _player.getProperty('video-params/primaries');
if (primaries?.contains('bt.2020') == true) {
return true;
}
// Check for HDR transfer characteristics
final gamma = await _player.getProperty('video-params/gamma');
if (gamma?.contains('pq') == true || gamma?.contains('hlg') == true) {
return true;
}
return false;
} catch (e) {
appLogger.d('ShaderService: HDR detection failed', error: e);
return false;
}
}
Future<void> disable() async {
await applyPreset(ShaderPreset.none);
}
}