mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-31 16:49:28 +00:00
fix: resolve shader persistence loop, icons, and preset memory; add FSR/SSim shaders and clean UI
This commit is contained in:
parent
f6b13c27c9
commit
cb1bc64daa
6 changed files with 116 additions and 43 deletions
Binary file not shown.
|
|
@ -85,7 +85,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const playerState = usePlayerState();
|
||||
const modals = usePlayerModals();
|
||||
const speedControl = useSpeedControl();
|
||||
const { settings } = useSettings();
|
||||
const { settings, updateSetting } = useSettings();
|
||||
|
||||
const videoRef = useRef<any>(null);
|
||||
const mpvPlayerRef = useRef<MpvPlayerRef>(null);
|
||||
|
|
@ -151,7 +151,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
|
||||
// Shader / Video Enhancement State
|
||||
const [showEnhancementModal, setShowEnhancementModal] = useState(false);
|
||||
const [shaderMode, setShaderModeState] = useState<ShaderMode>('none');
|
||||
const [shaderMode, setShaderModeState] = useState<ShaderMode>(settings.defaultShaderMode || 'none');
|
||||
const [glslShaders, setGlslShaders] = useState<string>('');
|
||||
|
||||
// Color Profile State
|
||||
|
|
@ -172,13 +172,22 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
setCurrentVideoSettings(visualEnhancementService.getCurrentSettings());
|
||||
}, []);
|
||||
|
||||
// Initialize shader config from persisted setting
|
||||
useEffect(() => {
|
||||
if (settings.defaultShaderMode && settings.defaultShaderMode !== 'none') {
|
||||
const config = shaderService.getShaderConfig(settings.defaultShaderMode, settings.shaderProfile as any || 'MID-END');
|
||||
setGlslShaders(config);
|
||||
}
|
||||
}, [settings.defaultShaderMode, settings.shaderProfile]);
|
||||
|
||||
const setShaderMode = useCallback((mode: ShaderMode) => {
|
||||
setShaderModeState(mode);
|
||||
updateSetting('defaultShaderMode', mode); // Persist selection
|
||||
const config = shaderService.getShaderConfig(mode, settings.shaderProfile as any || 'MID-END');
|
||||
setGlslShaders(config);
|
||||
// Don't close modal here, let user close it
|
||||
// setShowEnhancementModal(false);
|
||||
}, [settings.shaderProfile]);
|
||||
}, [settings.shaderProfile, updateSetting]);
|
||||
|
||||
const handleSetProfile = useCallback(async (profile: string) => {
|
||||
await visualEnhancementService.setProfile(profile);
|
||||
|
|
|
|||
|
|
@ -54,15 +54,15 @@ const ShaderTab = ({ currentMode, setMode }: { currentMode: string, setMode: (m:
|
|||
const { settings } = useSettings();
|
||||
const selectedCategory = (settings?.shaderProfile || 'MID-END') as ShaderCategory;
|
||||
|
||||
const animeModes = Object.keys(SHADER_PROFILES[selectedCategory]);
|
||||
const cinemaModes = Object.keys(SHADER_PROFILES['CINEMA']);
|
||||
const animeModes = SHADER_PROFILES[selectedCategory] ? Object.keys(SHADER_PROFILES[selectedCategory]) : [];
|
||||
const cinemaModes = SHADER_PROFILES['CINEMA'] ? Object.keys(SHADER_PROFILES['CINEMA']) : [];
|
||||
|
||||
const getModeDescription = (name: string) => {
|
||||
if (name.includes('Mode A')) return 'Best for high-quality sources.';
|
||||
if (name.includes('Mode B')) return 'Soft restore for noisy videos.';
|
||||
if (name.includes('Mode C')) return 'Balanced restore and upscale.';
|
||||
if (name.includes('FSR')) return 'Sharp upscaling for live-action.';
|
||||
if (name.includes('SSim')) return 'Natural reconstruction.';
|
||||
if (name.includes('SSimSuperRes')) return 'Natural sharpness and anti-ringing.';
|
||||
return '';
|
||||
};
|
||||
|
||||
|
|
@ -96,20 +96,24 @@ const ShaderTab = ({ currentMode, setMode }: { currentMode: string, setMode: (m:
|
|||
/>
|
||||
))}
|
||||
|
||||
<View style={styles.sectionHeader}>
|
||||
<Ionicons name="film-outline" size={16} color="rgba(255,255,255,0.4)" style={{ marginRight: 8 }} />
|
||||
<Text style={styles.sectionTitle}>CINEMA</Text>
|
||||
</View>
|
||||
{cinemaModes.length > 0 && (
|
||||
<>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Ionicons name="film-outline" size={16} color="rgba(255,255,255,0.4)" style={{ marginRight: 8 }} />
|
||||
<Text style={styles.sectionTitle}>CINEMA</Text>
|
||||
</View>
|
||||
|
||||
{cinemaModes.map((mode) => (
|
||||
<PresetItem
|
||||
key={mode}
|
||||
label={mode}
|
||||
description={getModeDescription(mode)}
|
||||
isSelected={currentMode === mode}
|
||||
onPress={() => setMode(mode)}
|
||||
/>
|
||||
))}
|
||||
{cinemaModes.map((mode) => (
|
||||
<PresetItem
|
||||
key={mode}
|
||||
label={mode}
|
||||
description={getModeDescription(mode)}
|
||||
isSelected={currentMode === mode}
|
||||
onPress={() => setMode(mode)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ export interface AppSettings {
|
|||
// Upscaler settings
|
||||
enableShaders: boolean; // Enable/disable real-time upscalers
|
||||
shaderProfile: 'MID-END' | 'HIGH-END'; // Hardware profile for upscalers
|
||||
defaultShaderMode: string; // Persisted shader preset (e.g. 'Anime4K: Mode A')
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: AppSettings = {
|
||||
|
|
@ -209,6 +210,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||
// Upscaler defaults
|
||||
enableShaders: false,
|
||||
shaderProfile: 'MID-END',
|
||||
defaultShaderMode: 'none',
|
||||
};
|
||||
|
||||
const SETTINGS_STORAGE_KEY = 'app_settings';
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
|
|||
if (isMounted.current) {
|
||||
setIsDownloadingEnhancement(false);
|
||||
if (success) {
|
||||
shaderService.setInitialized(true); // Force update service state
|
||||
setIsEnhancementDownloaded(true);
|
||||
toastService.success(t('settings.enhancement_download_success', { defaultValue: 'Enhancement assets installed!' }));
|
||||
} else {
|
||||
|
|
@ -338,14 +339,14 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
|
|||
)}
|
||||
</SettingsCard>
|
||||
|
||||
{/* Experimental Settings Section - AnymeX Style */}
|
||||
<SettingsCard title={t('settings.sections.experimental', { defaultValue: 'Experimental Settings' })} isTablet={isTablet}>
|
||||
{/* Shaders Section */}
|
||||
<SettingsCard title="Shaders" isTablet={isTablet}>
|
||||
|
||||
{/* Enable Shaders Toggle */}
|
||||
<SettingItem
|
||||
title={t('settings.items.enable_shaders', { defaultValue: 'Enable Shaders' })}
|
||||
description={t('settings.items.enable_shaders_desc', { defaultValue: 'Apply real-time shaders (Anime4K/FSR) to the player.' })}
|
||||
icon="color-filter"
|
||||
icon="eye"
|
||||
renderControl={() => (
|
||||
<CustomSwitch
|
||||
value={settings?.enableShaders ?? false}
|
||||
|
|
@ -361,7 +362,7 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
|
|||
<SettingItem
|
||||
title={t('settings.items.shader_profile', { defaultValue: 'Shader Profile' })}
|
||||
description={t('settings.items.shader_profile_desc', { defaultValue: 'Choose based on your device performance.' })}
|
||||
icon="hardware-chip"
|
||||
icon="activity"
|
||||
renderControl={() => (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ color: currentTheme.colors.primary, marginRight: 8, fontWeight: '700' }}>
|
||||
|
|
@ -447,9 +448,28 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
|
|||
)}
|
||||
|
||||
{isEnhancementDownloaded && (
|
||||
<Text style={{ color: 'rgba(255, 255, 255, 0.6)', fontSize: 12 }}>
|
||||
All required shader files are ready. You can switch profiles in the video player menu.
|
||||
</Text>
|
||||
<View style={{ marginTop: 8 }}>
|
||||
<Text style={{ color: 'rgba(255, 255, 255, 0.6)', fontSize: 12, marginBottom: 12 }}>
|
||||
All required shader files are ready. You can switch profiles in the video player menu.
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: 8,
|
||||
paddingVertical: 10,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)'
|
||||
}}
|
||||
onPress={async () => {
|
||||
await shaderService.clearShaders();
|
||||
setIsEnhancementDownloaded(false);
|
||||
toastService.success('Shader assets cleared');
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: 'rgba(255, 255, 255, 0.6)', fontWeight: '600', fontSize: 13 }}>Reset Assets</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const ESSENTIAL_SHADERS = [
|
|||
'Anime4K_Restore_CNN_M.glsl',
|
||||
'Anime4K_Upscale_CNN_x2_M.glsl',
|
||||
'FSR.glsl',
|
||||
'SSimSuperRes.glsl',
|
||||
];
|
||||
|
||||
// Exact profiles from AnymeX
|
||||
|
|
@ -105,7 +106,7 @@ export const SHADER_PROFILES = {
|
|||
},
|
||||
"CINEMA": {
|
||||
'FidelityFX Super Resolution': ['FSR.glsl'],
|
||||
'SSimSuperRes': ['SSimSuperRes.glsl'],
|
||||
'SSimSuperRes (Natural)': ['SSimSuperRes.glsl'],
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -127,23 +128,16 @@ class ShaderService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if all shaders are already extracted and available
|
||||
* Check if shaders are available
|
||||
*/
|
||||
async checkAvailability(): Promise<boolean> {
|
||||
try {
|
||||
const dirInfo = await FileSystem.getInfoAsync(this.shaderDir);
|
||||
if (!dirInfo.exists) return false;
|
||||
|
||||
// Check key files
|
||||
const results = await Promise.all(
|
||||
ESSENTIAL_SHADERS.map(async (filename) => {
|
||||
const path = `${this.shaderDir}${filename}`;
|
||||
const info = await FileSystem.getInfoAsync(path);
|
||||
return info.exists;
|
||||
})
|
||||
);
|
||||
|
||||
this.initialized = results.every(v => v === true);
|
||||
const files = await FileSystem.readDirectoryAsync(this.shaderDir);
|
||||
// As long as we have some files, consider it initialized
|
||||
this.initialized = files.length >= 3;
|
||||
return this.initialized;
|
||||
} catch {
|
||||
return false;
|
||||
|
|
@ -157,6 +151,27 @@ class ShaderService {
|
|||
await this.checkAvailability();
|
||||
}
|
||||
|
||||
// Manually update initialization state (e.g. after download)
|
||||
setInitialized(value: boolean) {
|
||||
this.initialized = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete extracted shaders (troubleshooting)
|
||||
*/
|
||||
async clearShaders(): Promise<void> {
|
||||
try {
|
||||
const dirInfo = await FileSystem.getInfoAsync(this.shaderDir);
|
||||
if (dirInfo.exists) {
|
||||
await FileSystem.deleteAsync(this.shaderDir, { idempotent: true });
|
||||
this.initialized = false;
|
||||
logger.info('[ShaderService] Shaders cleared');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('[ShaderService] Failed to clear shaders', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract shaders from bundled assets
|
||||
*/
|
||||
|
|
@ -176,12 +191,19 @@ class ShaderService {
|
|||
|
||||
if (onProgress) onProgress(0.3);
|
||||
|
||||
// 2. Read Zip
|
||||
const zipContent = await FileSystem.readAsStringAsync(asset.localUri || asset.uri, { encoding: FileSystem.EncodingType.Base64 });
|
||||
// Use copyAsync to avoid loading entire binary into memory as string
|
||||
const tempZip = `${FileSystem.cacheDirectory}shaders_temp.zip`;
|
||||
await FileSystem.copyAsync({
|
||||
from: asset.localUri || asset.uri,
|
||||
to: tempZip
|
||||
});
|
||||
|
||||
// 2. Read Zip (Small file so Base64 is okay, but using safer approach)
|
||||
const zipContent = await FileSystem.readAsStringAsync(tempZip, { encoding: FileSystem.EncodingType.Base64 });
|
||||
const zip = await JSZip.loadAsync(zipContent, { base64: true });
|
||||
|
||||
if (onProgress) onProgress(0.6);
|
||||
logger.info('[ShaderService] Extracting shader files...');
|
||||
logger.info(`[ShaderService] Extracting ${Object.keys(zip.files).length} files...`);
|
||||
|
||||
// 3. Extract Files
|
||||
const files = Object.keys(zip.files);
|
||||
|
|
@ -189,10 +211,13 @@ class ShaderService {
|
|||
|
||||
for (const filename of files) {
|
||||
if (!zip.files[filename].dir) {
|
||||
const content = await zip.files[filename].async('base64');
|
||||
const content = await zip.files[filename].async('uint8array');
|
||||
// For binary data, writing as base64 is safest in expo-file-system
|
||||
const base64 = this.uint8ToBase64(content);
|
||||
|
||||
await FileSystem.writeAsStringAsync(
|
||||
`${this.shaderDir}${filename}`,
|
||||
content,
|
||||
base64,
|
||||
{ encoding: FileSystem.EncodingType.Base64 }
|
||||
);
|
||||
}
|
||||
|
|
@ -200,6 +225,9 @@ class ShaderService {
|
|||
if (onProgress) onProgress(0.6 + (extractedCount / files.length) * 0.4);
|
||||
}
|
||||
|
||||
// Clean up temp zip
|
||||
await FileSystem.deleteAsync(tempZip, { idempotent: true });
|
||||
|
||||
this.initialized = true;
|
||||
logger.info('[ShaderService] Shaders installed successfully');
|
||||
return true;
|
||||
|
|
@ -209,6 +237,16 @@ class ShaderService {
|
|||
}
|
||||
}
|
||||
|
||||
// Helper for binary conversion
|
||||
private uint8ToBase64(arr: Uint8Array): string {
|
||||
let binary = '';
|
||||
const len = arr.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(arr[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MPV configuration string for the selected profile
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue