import { forwardRef, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import type { ImageData, LoopMode } from "../types.ts"; interface ImageCarouselProps { images: ImageData["images"]; currentIndex: number; isPlaying: boolean; segmentProgress: number; mediaSyncToken: number; loopMode: LoopMode; onTogglePlay: () => void; onAnimatedDuration?: (imageId: string, durationMs: number) => void; } export const ImageCarousel = forwardRef( ( { images, currentIndex, isPlaying, segmentProgress, mediaSyncToken, loopMode, onTogglePlay, onAnimatedDuration, }, ref, ) => { const videoRefs = useRef>(new Map()); const activeVideoIdRef = useRef(null); const segmentProgressRef = useRef(segmentProgress); const [useLightweightMode, setUseLightweightMode] = useState(false); useEffect(() => { segmentProgressRef.current = segmentProgress; }, [segmentProgress]); const syncVideoToSegmentProgress = useCallback( (videoEl: HTMLVideoElement, progress: number, force = false) => { const duration = videoEl.duration; if (!Number.isFinite(duration) || duration <= 0) return; const clampedProgress = Math.max(0, Math.min(1, progress)); const maxTime = Math.max(0, duration - 0.05); const targetTime = Math.min(maxTime, duration * clampedProgress); if (force || Math.abs(videoEl.currentTime - targetTime) > 0.12) { try { videoEl.currentTime = targetTime; } catch {} } }, [], ); useEffect(() => { const userAgent = navigator.userAgent; const isIOS = /iPad|iPhone|iPod/.test(userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1); const isSafari = /Safari/.test(userAgent) && !/CriOS|FxiOS|EdgiOS/.test(userAgent); setUseLightweightMode(isIOS && isSafari); }, []); // 虚拟滚动:只渲染当前图片和前后各一张 const visibleIndices = useMemo(() => { if (useLightweightMode) return [currentIndex]; const indices: number[] = []; if (currentIndex > 0) indices.push(currentIndex - 1); indices.push(currentIndex); if (currentIndex < images.length - 1) indices.push(currentIndex + 1); return indices; }, [currentIndex, images.length, useLightweightMode]); // iOS Safari 对多个同屏 video 很敏感,这里只让当前动图持有 video 元素并跟随全局播放状态。 useEffect(() => { const currentImage = images[currentIndex]; if (!currentImage?.animated) { activeVideoIdRef.current = null; videoRefs.current.forEach((videoEl) => videoEl.pause()); return; } const videoEl = videoRefs.current.get(currentImage.id); videoRefs.current.forEach((itemVideoEl, imageId) => { if (imageId !== currentImage.id || !isPlaying) { itemVideoEl.pause(); } }); if (videoEl) { if (activeVideoIdRef.current !== currentImage.id) { activeVideoIdRef.current = currentImage.id; syncVideoToSegmentProgress(videoEl, segmentProgressRef.current, true); } if (isPlaying) { videoEl.play().catch(() => {}); } else { videoEl.pause(); } } }, [currentIndex, images, isPlaying, loopMode, syncVideoToSegmentProgress]); useEffect(() => { const currentImage = images[currentIndex]; if (!currentImage?.animated) return; const videoEl = videoRefs.current.get(currentImage.id); if (!videoEl) return; syncVideoToSegmentProgress(videoEl, segmentProgressRef.current, true); if (isPlaying) videoEl.play().catch(() => {}); }, [ currentIndex, images, isPlaying, loopMode, mediaSyncToken, syncVideoToSegmentProgress, ]); const handleVideoRef = (el: HTMLVideoElement | null, imageId: string) => { if (el) { videoRefs.current.set(imageId, el); } else { videoRefs.current.delete(imageId); } }; return (
{visibleIndices.map((i) => { const img = images[i]; const isCurrent = i === currentIndex; const animatedSrc = isCurrent ? img.animated : null; return (
{animatedSrc ? (
); })}
); }, ); ImageCarousel.displayName = "ImageCarousel";