/** * Компонент управления синхронизацией мультикамерных углов */ import { AlertCircle, Check, Clock, Hash, Loader2, Music, Wand2 } from "lucide-react" import { useCallback, useState } from "react" import { Alert, AlertDescription } from "@/components/ui/alert" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Slider } from "@/components/ui/slider" import { cn } from "@/lib/utils" import { useMulticam } from "../hooks/use-multicam" import { AudioSyncDialog } from "./audio-sync-dialog" interface SyncControlsProps { baseClipId: string className?: string onSyncComplete?: () => void } interface ManualSyncState { isOpen: boolean angleIndex: number | null currentOffset: number } export function SyncControls({ baseClipId, className, onSyncComplete }: SyncControlsProps) { const multicam = useMulticam(baseClipId) const [isSyncing, setIsSyncing] = useState(false) const [syncMethod, setSyncMethod] = useState<"timecode" | "audio" | "manual" | null>(null) const [syncStatus, setSyncStatus] = useState<"idle" | "syncing" | "success" | "error">("idle") const [audioSyncOpen, setAudioSyncOpen] = useState(false) const [manualSync, setManualSync] = useState({ isOpen: false, angleIndex: null, currentOffset: 0, }) // Синхронизация по таймкоду const handleTimecodeSync = useCallback(async () => { setIsSyncing(true) setSyncMethod("timecode") setSyncStatus("syncing") try { await new Promise((resolve) => setTimeout(resolve, 500)) // Имитация задержки void multicam.autoSyncByTimecode() setSyncStatus("success") onSyncComplete?.() // Сбросить статус через 3 секунды setTimeout(() => { setSyncStatus("idle") setSyncMethod(null) }, 3000) } catch (error) { console.error("[SyncControls] Timecode sync failed:", error) setSyncStatus("error") } finally { setIsSyncing(false) } }, [multicam, onSyncComplete]) // Синхронизация по аудио const handleAudioSync = useCallback(() => { setAudioSyncOpen(true) }, []) // Открытие диалога ручной синхронизации const handleManualSync = useCallback( (angleIndex: number) => { const currentOffset = multicam.syncOffsets[angleIndex] || 0 setManualSync({ isOpen: true, angleIndex, currentOffset, }) }, [multicam.syncOffsets], ) // Применение ручной синхронизации const applyManualSync = useCallback(() => { if (manualSync.angleIndex !== null) { multicam.setSyncOffset(manualSync.angleIndex, manualSync.currentOffset) multicam.syncAngles() setManualSync({ isOpen: false, angleIndex: null, currentOffset: 0 }) onSyncComplete?.() } }, [manualSync, multicam, onSyncComplete]) // Получение иконки для статуса const getStatusIcon = () => { switch (syncStatus) { case "syncing": return case "success": return case "error": return default: return } } // Получение текста статуса const getStatusText = () => { if (syncStatus === "syncing") { switch (syncMethod) { case "timecode": return "Синхронизация по таймкоду..." case "audio": return "Анализ аудио..." case "manual": return "Применение смещения..." default: return "Синхронизация..." } } if (syncStatus === "success") { return "Синхронизировано!" } if (syncStatus === "error") { return "Ошибка синхронизации" } return "Синхронизация" } // Обработчик завершения аудио синхронизации const handleAudioSyncComplete = useCallback(async () => { setAudioSyncOpen(false) setSyncStatus("success") onSyncComplete?.() // Сбросить статус через 3 секунды setTimeout(() => { setSyncStatus("idle") setSyncMethod(null) }, 3000) }, [onSyncComplete]) if (!multicam.hasMulticamSupport) { return null } return ( <> Синхронизация по таймкоду Синхронизация по аудио
Ручная синхронизация
{multicam.angles.map((angle, index) => ( handleManualSync(index)} disabled={isSyncing || index === multicam.activeAngleIndex} > {angle.name}{" "} {Math.abs(multicam.syncOffsets[index] || 0) > 0.01 && `(${multicam.syncOffsets[index] > 0 ? "+" : ""}${multicam.syncOffsets[index]?.toFixed(2)}s)`} ))}
{/* Диалог ручной синхронизации */} { if (!open) { setManualSync({ isOpen: false, angleIndex: null, currentOffset: 0 }) } }} > Ручная синхронизация {manualSync.angleIndex !== null && ( <>Настройте смещение для камеры "{multicam.angles[manualSync.angleIndex]?.name}" )}
Смещение {manualSync.currentOffset > 0 ? "+" : ""} {manualSync.currentOffset.toFixed(3)}s
{ setManualSync((prev) => ({ ...prev, currentOffset: value })) }} min={-5} max={5} step={0.001} className="w-full" />
-5s 0s +5s
Используйте положительные значения, чтобы сдвинуть клип вперед, и отрицательные — чтобы сдвинуть назад относительно базового угла.
{/* Диалог аудио синхронизации */} setAudioSyncOpen(false)} onSync={async () => { const results = await multicam.autoSyncByAudio() void handleAudioSyncComplete() return results || [] }} angleCount={multicam.angles.length} /> ) }