import { useDraggable } from "@dnd-kit/core" import { Film } from "lucide-react" import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react" import { useResources } from "@/features" import { useMediaPreview } from "@/features/media/hooks/use-media-preview" import { FfprobeStream } from "@/features/media/types/ffprobe" import { MediaFile } from "@/features/media/types/media" import { calculateAdaptiveWidth, calculateWidth, parseRotation } from "@/features/media/utils/video" import { TimelineResource } from "@/features/resources/types" import { DragData } from "@/features/timeline/types/drag-drop" import { getTrackTypeForMediaFile } from "@/features/timeline/utils/drag-calculations" import { usePlayer } from "@/features/video-player" import { formatDuration } from "@/lib/date" import { checkFileAccess, convertToAssetUrl, convertVideoSrc } from "@/lib/tauri-utils" import { cn, formatResolution } from "@/lib/utils" import { ApplyButton } from "../layout" import { AddMediaButton } from "../layout/add-media-button" import { FavoriteButton } from "../layout/favorite-button" interface VideoPreviewProps { file: MediaFile size?: number showFileName?: boolean dimensions?: [number, number] ignoreRatio?: boolean } /** * Предварительный просмотр видеофайла * * Функционал: * - Отображает превью видеофайла с поддержкой ленивой загрузки * - Адаптивный размер контейнера с соотношением сторон 16:9 * - Поддерживает два размера UI (стандартный и большой при size > 100) * - Опциональное отображение имени файла * - Кнопка добавления с состояниями (добавлено/не добавлено) * - Темная тема для UI элементов * * @param file - Объект файла с путем и метаданными * @param size - Размер превью в пикселях (по умолчанию 60) * @param showFileName - Флаг для отображения имени файла (по умолчанию false) * @param dimensions - Соотношение сторон контейнера [ширина, высота], по умолчанию [16, 9] * @param ignoreRatio - Флаг для игнорирования соотношения сторон (по умолчанию false) */ export const VideoPreview = memo( function VideoPreview({ file, size = 150, showFileName = false, ignoreRatio = false }: VideoPreviewProps) { const [isPlaying, setIsPlaying] = useState(false) const [hoverTime, setHoverTime] = useState(null) const [isLoaded, setIsLoaded] = useState(false) const [previewData, setPreviewData] = useState(null) const videoRefs = useRef>({}) const { isAdded: isResourceAdded } = useResources() const isAdded = isResourceAdded(file.id, "media") const { setPreviewMedia, playerSetSource, playerSetMedia } = usePlayer() // Используем Preview Manager для получения данных превью const { getPreviewData } = useMediaPreview() // Загружаем preview data при монтировании useEffect(() => { void getPreviewData(file.id).then((data) => { if (data?.browser_thumbnail?.base64_data) { setPreviewData(data.browser_thumbnail.base64_data) } }) }, [file.id, getPreviewData]) // Обработчик применения видео - теперь отправляем в главный плеер через backend const handleApplyVideo = useCallback( async (_resource: TimelineResource, _type: string) => { try { // Устанавливаем плеер в режим браузера await playerSetSource("browser") // Устанавливаем медиа в плеер await playerSetMedia(file.id, 0) console.log(`[VideoPreview] Media sent to main player: ${file.name}`) } catch (error) { console.error("[VideoPreview] Failed to send media to player:", error) // Fallback к старому поведению setPreviewMedia(file) } }, [file], ) // Используем useRef для хранения времени последнего обновления const lastUpdateTimeRef = useRef(0) // Создаем стабильные ключи для рефов useEffect(() => { const videoStreams = file.probeData?.streams.filter((s) => s.codec_type === "video") ?? [] videoStreams.forEach((stream) => { const key = stream.streamKey ?? `stream-${stream.index}` videoRefs.current[key] ??= null }) }, [file.probeData?.streams]) // Используем useRef для хранения hoverTime вместо useState // чтобы избежать ререндеров при движении мыши const hoverTimeRef = useRef(null) // Setup draggable functionality const dragData: DragData = useMemo( () => ({ type: getTrackTypeForMediaFile(file) === "video" ? "video" : getTrackTypeForMediaFile(file) === "audio" ? "audio" : "image", mediaFile: file, }), [file], ) const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: `video-${file.id}`, data: dragData, }) const handleMouseMove = useCallback( (e: React.MouseEvent, stream: FfprobeStream) => { // Не обновляем состояние во время воспроизведения if (isPlaying) return const now = Date.now() // Ограничиваем обновления до 30 fps для производительности if (now - lastUpdateTimeRef.current < 33) return lastUpdateTimeRef.current = now const rect = e.currentTarget.getBoundingClientRect() const x = e.clientX - rect.left const percentage = x / rect.width const newTime = percentage * (file.duration ?? 0) // Обновляем ref hoverTimeRef.current = newTime // Обновляем состояние с дебаунсингом setHoverTime(newTime) const key = stream.streamKey ?? `stream-${stream.index}` const videoRef = videoRefs.current[key] if (videoRef && !isPlaying) { videoRef.currentTime = newTime } }, [file.duration, isPlaying], ) const handleMouseLeave = useCallback(() => { setHoverTime(null) // При уходе мыши останавливаем воспроизведение всех видео, кроме видео в шаблоне if (isPlaying) { setIsPlaying(false) } }, [isPlaying]) // Функция handlePlayPause - теперь отправляем видео в главный плеер const handlePlayPause = useCallback( async (e: React.MouseEvent, stream: FfprobeStream) => { e.preventDefault() try { // Отправляем видео в главный плеер через backend await playerSetSource("browser") await playerSetMedia(file.id, hoverTime || 0) console.log(`[VideoPreview] Video sent to main player from preview: ${file.name} at time ${hoverTime || 0}`) } catch (error) { console.error("[VideoPreview] Failed to send video to main player:", error) // Fallback: локальное воспроизведение в превью (старое поведение) const key = stream.streamKey ?? `stream-${stream.index}` const videoRef = videoRefs.current[key] if (!videoRef) return const newPlayingState = !isPlaying if (newPlayingState) { if (hoverTime !== null) { videoRef.currentTime = hoverTime } videoRef .play() .catch((err: unknown) => console.error("[VideoPreview] Ошибка воспроизведения в превью:", err)) } else { videoRef.pause() } setIsPlaying(newPlayingState) console.log( `[VideoPreview] Fallback: Видео ${newPlayingState ? "запущено" : "остановлено"} в превью:`, file.name, ) } }, [hoverTime, file, playerSetSource, playerSetMedia, isPlaying], ) // Состояние для хранения объекта URL const [videoUrl, setVideoUrl] = useState("") // Мемоизируем URL, чтобы он не менялся без необходимости const memoizedVideoUrl = useMemo(() => videoUrl, [videoUrl]) // Функция для получения URL видео без загрузки в память const loadVideoFile = useCallback(async (path: string) => { console.log(`[VideoPreview] loadVideoFile called with path: ${path}`) console.log( `[VideoPreview] isTauriEnvironment: ${typeof window !== "undefined" && window.__TAURI__ !== undefined}`, ) console.log( "[VideoPreview] window.__TAURI__:", typeof window !== "undefined" ? window.__TAURI__ : "window undefined", ) // Используем file:// протокол для видео через convertVideoSrc const videoUrl = convertVideoSrc(path) console.log("[VideoPreview] Converting path:") console.log(` Original: ${path}`) console.log(` Video URL: ${videoUrl}`) console.log(` URL starts with asset://: ${videoUrl.startsWith("asset://")}`) return videoUrl }, []) // Мемоизируем путь к файлу для предотвращения лишних перезагрузок const filePath = useMemo(() => file.path, [file.path]) // Эффект для загрузки видео при монтировании компонента useEffect(() => { let isMounted = true console.log(`[VideoPreview] Attempting to load video from path: ${filePath}`) // Проверяем доступ к файлу через Tauri API void checkFileAccess(filePath).then((hasAccess) => { console.log(`[VideoPreview] File access check result: ${hasAccess}`) if (!hasAccess) { console.error(`[VideoPreview] No access to file: ${filePath}`) // Попробуем загрузить все равно, может проблема в проверке доступа } void loadVideoFile(filePath).then((url) => { if (isMounted) { console.log(`[VideoPreview] Video URL generated: ${url}`) console.log(`[VideoPreview] URL protocol: ${url.split(":")[0]}`) setVideoUrl(url) } }) }) // Очистка при размонтировании компонента return () => { isMounted = false } }, [filePath, loadVideoFile]) // Используем мемоизированный путь // Оптимизируем вычисления с помощью useMemo const videoData = useMemo(() => { const videoStreams = file.probeData?.streams.filter((s) => s.codec_type === "video") ?? [] const isMultipleStreams = videoStreams.length > 1 return { videoStreams, isMultipleStreams } }, [file.probeData?.streams]) // Transform style for drag feedback const style = transform ? { transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, opacity: isDragging ? 0.5 : 1, } : undefined return (
{videoData.videoStreams.length === 0 ? ( // Плейсхолдер с соотношением 16:9 пока метаданные не загрузились
{ e.preventDefault() try { // Отправляем видео в главный плеер await playerSetSource("browser") await playerSetMedia(file.id, hoverTime || 0) console.log( `[VideoPreview] Video sent to main player from placeholder: ${file.name} at time ${hoverTime || 0}`, ) } catch (error) { console.error("[VideoPreview] Failed to send video to main player from placeholder:", error) // Fallback: локальное воспроизведение const video = e.currentTarget.querySelector("video") if (!video) return const newPlayingState = !isPlaying if (newPlayingState) { video.play().catch((err: unknown) => console.error("[VideoPreview] Ошибка воспроизведения:", err)) } else { video.pause() } setIsPlaying(newPlayingState) console.log( `[VideoPreview] Fallback: Видео ${newPlayingState ? "запущено" : "остановлено"} в плейсхолдере:`, file.name, ) } }} onMouseMove={(e) => { const rect = e.currentTarget.getBoundingClientRect() const x = e.clientX - rect.left const percentage = x / rect.width const newTime = percentage * (file.duration ?? 0) setHoverTime(newTime) const video = e.currentTarget.querySelector("video") if (video) { video.currentTime = newTime } }} onMouseLeave={() => { setHoverTime(null) if (isPlaying) { setIsPlaying(false) } }} >
) : ( videoData.videoStreams.map((stream: FfprobeStream) => { const key = stream.streamKey ?? `stream-${stream.index}` const isMultipleStreams = videoData.isMultipleStreams // Используем размеры из метаданных или значения по умолчанию для 16:9 const videoWidth = stream.width || 1920 const videoHeight = stream.height || 1080 const width = calculateWidth(videoWidth, videoHeight, size, parseRotation(stream.rotation)) const adptivedWidth = calculateAdaptiveWidth( width, isMultipleStreams, stream.display_aspect_ratio || "16:9", ) // Используем соотношение сторон из метаданных или 16:9 по умолчанию const aspectRatio = stream.display_aspect_ratio?.split(":").map(Number) ?? [16, 9] const ratio = aspectRatio[0] / aspectRatio[1] return (
1 ? ignoreRatio ? width : adptivedWidth : isMultipleStreams && ignoreRatio ? width : adptivedWidth, }} >
handleMouseMove(e, stream)} onMouseLeave={handleMouseLeave} onClick={(e) => handlePlayPause(e, stream)} style={{ backgroundColor: "#1a1a1a" }} >
) }) )}
) }, (prevProps, nextProps) => { // Сравниваем только важные свойства для предотвращения лишних перерендеров const isSameFile = prevProps.file.path === nextProps.file.path const isSameMetadataState = prevProps.file.isLoadingMetadata === nextProps.file.isLoadingMetadata const isSameThumbnail = prevProps.file.thumbnailPath === nextProps.file.thumbnailPath const isSameProps = prevProps.size === nextProps.size && prevProps.showFileName === nextProps.showFileName && prevProps.ignoreRatio === nextProps.ignoreRatio // Сравниваем количество потоков (главный индикатор изменения метаданных) const prevStreamsCount = prevProps.file.probeData?.streams?.length ?? 0 const nextStreamsCount = nextProps.file.probeData?.streams?.length ?? 0 const isSameStreamsCount = prevStreamsCount === nextStreamsCount const shouldSkipRender = !nextProps.file.isLoadingMetadata && isSameStreamsCount && isSameFile && isSameProps && isSameThumbnail if (!shouldSkipRender) { console.log(`[VideoPreview] Re-rendering ${nextProps.file.name}:`, { isSameFile, isSameMetadataState, isSameThumbnail, isSameProps, isSameStreamsCount, isLoadingMetadata: nextProps.file.isLoadingMetadata, }) } return shouldSkipRender }, )