/** * Effects Preview Player Component * * Видеоплеер с real-time предпросмотром эффектов: * - WebGL-ускоренная обработка эффектов * - Интерактивное изменение параметров * - Предпросмотр цепочки эффектов * - Оптимизация производительности */ import { useCallback, useEffect, useRef, useState } from "react" import { toast } from "sonner" import { AspectRatio } from "@/components/ui/aspect-ratio" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Slider } from "@/components/ui/slider" import { Switch } from "@/components/ui/switch" import { TooltipProvider } from "@/components/ui/tooltip" import { useProjectSettings } from "@/features/project-settings" import { TimelineClip } from "@/features/timeline/types/timeline" import { convertVideoSrc } from "@/lib/tauri-utils" import { EffectPreviewOptions, getEffectsPreviewService } from "../services/effects-preview" import { usePlayer } from "../services/player-provider" import { PlayerControls } from "./player-controls" interface EffectParameter { name: string type: "float" | "vec2" | "vec3" | "vec4" | "int" | "bool" value: any min?: number max?: number step?: number } interface EffectSettings { effectId: string enabled: boolean parameters: Record intensity: number } export function EffectsPreviewPlayer() { const { settings: { aspectRatio }, } = useProjectSettings() const { currentVideo: video, currentTime } = usePlayer() const videoRef = useRef(null) const effectsCanvasRef = useRef(null) const [showEffectsPanel, setShowEffectsPanel] = useState(false) const [isPreviewActive, setIsPreviewActive] = useState(false) // Effects settings const [activeEffects, setActiveEffects] = useState([]) const [availableEffects, setAvailableEffects] = useState>([]) const [selectedEffectId, setSelectedEffectId] = useState("") // Preview options const [previewOptions, setPreviewOptions] = useState({ enabled: true, quality: "medium", realTime: true, cacheResults: false, maxFrameRate: 30, }) // Mock clip for demonstration const mockClip: TimelineClip = { id: "demo-clip", name: "Demo Clip", mediaId: video?.id || "preview-media", mediaFile: video || undefined, trackId: "preview-track", startTime: 0, duration: 10, mediaStartTime: 0, mediaEndTime: 10, offset: 0, volume: 1.0, speed: 1.0, isReversed: false, opacity: 1.0, isSelected: false, isLocked: false, createdAt: new Date(), updatedAt: new Date(), effects: activeEffects.map((effect, index) => ({ id: `${effect.effectId}-${Date.now()}`, effectId: effect.effectId, enabled: effect.enabled, parameters: effect.parameters, intensity: effect.intensity, startTime: 0, endTime: 10, order: index, })), filters: [], transitions: [], } // Вычисляем соотношение сторон const aspectRatioValue = aspectRatio.value.width / aspectRatio.value.height /** * Инициализация эффектов */ useEffect(() => { const effects = getEffectsPreviewService().getAvailableEffects() setAvailableEffects(effects) if (effects.length > 0 && !selectedEffectId) { setSelectedEffectId(effects[0].id) } }, [selectedEffectId]) /** * Обновление canvas размеров */ useEffect(() => { if (effectsCanvasRef.current && videoRef.current) { const canvas = effectsCanvasRef.current const video = videoRef.current canvas.width = video.videoWidth || 1920 canvas.height = video.videoHeight || 1080 } }, [video]) /** * Запуск/остановка preview */ useEffect(() => { if (!videoRef.current || !effectsCanvasRef.current || !video?.path) return if (isPreviewActive && previewOptions.enabled && activeEffects.length > 0) { getEffectsPreviewService().startRealTimePreview( videoRef.current, mockClip, effectsCanvasRef.current, previewOptions, ) } else { getEffectsPreviewService().stopRealTimePreview() } return () => { getEffectsPreviewService().stopRealTimePreview() } }, [isPreviewActive, previewOptions, activeEffects, mockClip, video?.path]) /** * Добавление эффекта */ const addEffect = useCallback((effectId: string) => { const parameters = getEffectsPreviewService().getEffectParameters(effectId) if (!parameters) return const newEffect: EffectSettings = { effectId, enabled: true, parameters, intensity: 1.0, } setActiveEffects((prev) => [...prev, newEffect]) toast.success(`Эффект "${effectId}" добавлен`) }, []) /** * Удаление эффекта */ const removeEffect = useCallback((index: number) => { setActiveEffects((prev) => prev.filter((_, i) => i !== index)) }, []) /** * Обновление параметров эффекта */ const updateEffectParameter = useCallback((effectIndex: number, parameterName: string, value: any) => { setActiveEffects((prev) => prev.map((effect, index) => { if (index === effectIndex) { return { ...effect, parameters: { ...effect.parameters, [parameterName]: value, }, } } return effect }), ) }, []) /** * Переключение эффекта */ const toggleEffect = useCallback((index: number) => { setActiveEffects((prev) => prev.map((effect, i) => { if (i === index) { return { ...effect, enabled: !effect.enabled } } return effect }), ) }, []) /** * Изменение интенсивности эффекта */ const updateEffectIntensity = useCallback((index: number, intensity: number) => { setActiveEffects((prev) => prev.map((effect, i) => { if (i === index) { return { ...effect, intensity } } return effect }), ) }, []) /** * Рендер параметров эффекта */ const renderEffectParameters = (effect: EffectSettings, index: number) => { return (

{effect.effectId}

toggleEffect(index)} />
{/* Intensity slider */}
updateEffectIntensity(index, value)} max={1} min={0} step={0.01} disabled={!effect.enabled} className="w-full" />
{/* Effect parameters */} {Object.entries(effect.parameters).map(([paramName, value]) => (
{typeof value === "number" && ( updateEffectParameter(index, paramName, newValue)} max={getParameterMax(paramName)} min={getParameterMin(paramName)} step={getParameterStep(paramName)} disabled={!effect.enabled} className="w-full" /> )}
))}
) } /** * Получение максимального значения для параметра */ const getParameterMax = (paramName: string): number => { switch (paramName) { case "brightness": return 1 case "contrast": return 3 case "saturation": return 3 case "hue": return 360 case "blurRadius": return 20 case "vignette": return 2 case "sepia": return 1 case "noise": return 1 case "glitchStrength": return 1 case "colorSeparation": return 1 default: return 2 } } const getParameterMin = (paramName: string): number => { switch (paramName) { case "brightness": return -1 case "contrast": return 0 case "saturation": return 0 case "hue": return -180 case "blurRadius": return 0 default: return 0 } } const getParameterStep = (paramName: string): number => { switch (paramName) { case "hue": return 1 case "blurRadius": return 0.5 default: return 0.01 } } if (!video?.path) { return (
Нет видео
) } return (
{/* Original video (hidden when effects active) */}
) } EffectsPreviewPlayer.displayName = "EffectsPreviewPlayer"