/** * Компонент для просмотра всех углов камер в мультикамерном режиме */ import { Camera, Pause, Play } from "lucide-react" import { useCallback, useEffect, useRef, useState } from "react" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" import type { MulticamAngle } from "../hooks/use-multicam" import { useMulticam } from "../hooks/use-multicam" import { SyncControls } from "./sync-controls" interface AngleViewerProps { /** * ID базового клипа для мультикамерной группы */ baseClipId: string /** * Максимальное количество колонок в сетке */ maxColumns?: number /** * Показывать ли метки камер */ showLabels?: boolean /** * Показывать ли таймкод */ showTimecode?: boolean /** * Обработчик клика по углу */ onAngleClick?: (angle: MulticamAngle, index: number) => void /** * Класс для контейнера */ className?: string } export function AngleViewer({ baseClipId, maxColumns = 2, showLabels = true, showTimecode = false, onAngleClick, className, }: AngleViewerProps) { const multicam = useMulticam(baseClipId) const videoRefs = useRef<(HTMLVideoElement | null)[]>([]) const [isPlaying, setIsPlaying] = useState(false) const [loadErrors, setLoadErrors] = useState>({}) // Вычисляем оптимальное количество колонок для сетки const getOptimalColumns = (count: number): number => { if (count <= 1) return 1 if (count <= 4) return 2 if (count <= 9) return 3 if (count <= 16) return 4 return Math.min(maxColumns, Math.ceil(Math.sqrt(count))) } const columns = getOptimalColumns(multicam.angles.length) // Обработчик клика по углу const handleAngleClick = useCallback( (angle: MulticamAngle, index: number) => { // Переключаемся на выбранный угол multicam.switchToAngle(index) // Вызываем внешний обработчик если есть onAngleClick?.(angle, index) }, [multicam, onAngleClick], ) // Управление воспроизведением const togglePlayback = useCallback(() => { const newIsPlaying = !isPlaying setIsPlaying(newIsPlaying) // Синхронизируем воспроизведение всех видео videoRefs.current.forEach((video) => { if (video) { if (newIsPlaying) { video.play().catch(console.error) } else { video.pause() } } }) }, [isPlaying]) // Синхронизация времени при изменении активного угла useEffect(() => { if (multicam.activeAngle) { const activeVideo = videoRefs.current[multicam.activeAngleIndex] if (activeVideo) { const currentTime = activeVideo.currentTime // Синхронизируем остальные видео videoRefs.current.forEach((video, index) => { if (video && index !== multicam.activeAngleIndex) { const offset = multicam.syncOffsets[index] || 0 video.currentTime = currentTime + offset } }) } } }, [multicam.activeAngleIndex, multicam.activeAngle, multicam.syncOffsets]) if (multicam.angles.length === 0) { return (

Нет доступных углов камер

) } return (
{/* Контролы воспроизведения и синхронизации */}
{ console.log("[AngleViewer] Sync completed") }} />
{/* Сетка с углами камер */}
{multicam.angles.map((angle, index) => (
handleAngleClick(angle, index)} > {/* Видео превью */}
{/* Показываем заглушку при ошибке загрузки */} {loadErrors[angle.id] && (
)} {/* Затемнение для неактивных */} {!angle.isActive && (
)}
{/* Метка камеры */} {showLabels && (
{angle.name}
)} {/* Номер камеры */}
{index + 1}
{/* Таймкод */} {showTimecode && (
00:00:00
)} {/* Индикатор синхронизации */} {multicam.isSync && Math.abs(multicam.syncOffsets[index] || 0) > 0.1 && (
{multicam.syncOffsets[index] > 0 ? "+" : ""} {multicam.syncOffsets[index]?.toFixed(1)}s
)}
))}
) }