import { forwardRef, memo, type RefObject, useCallback, useEffect, useMemo, useRef, } from "react"; import type { ImageData, LoopMode, ObjectFit } from "../types.ts"; import { releaseMediaElement } from "../utils"; interface ImageCarouselProps { images: ImageData["images"]; currentIndex: number; isPlaying: boolean; playbackRate?: number; segmentProgressRef: RefObject; mediaSyncToken: number; loopMode: LoopMode; hasNextNeighbor: boolean; rotation: number; objectFit: ObjectFit; failedAnimatedMap: Record; onTogglePlay: () => void; onAnimatedDuration: (imageId: string, durationMs: number) => void; onMediaReady: (imageId: string) => void; onAnimatedFailed: (imageId: string) => void; onAnimatedProgress: (imageId: string, progress: number) => void; onAnimatedEnded: (imageId: string) => void; } export const ImageCarousel = memo( forwardRef( ( { images, currentIndex, isPlaying, playbackRate = 1, segmentProgressRef, mediaSyncToken, loopMode, hasNextNeighbor, rotation, objectFit, failedAnimatedMap, onTogglePlay, onAnimatedDuration, onMediaReady, onAnimatedFailed, onAnimatedProgress, onAnimatedEnded, }, ref, ) => { const videoRefs = useRef>(new Map()); const videoRefCallbacks = useRef< Map void> >(new Map()); const activeVideoIdRef = useRef(null); const lastMediaSyncTokenRef = useRef(mediaSyncToken); const playbackRateRef = useRef(playbackRate); playbackRateRef.current = playbackRate; const pendingSeekRef = useRef<{ imageId: string; progress: number; } | null>(null); const shouldLoopCurrentVideo = loopMode === "single" || (images.length === 1 && (loopMode !== "sequential" || !hasNextNeighbor)); const seekVideoToProgress = useCallback( (videoEl: HTMLVideoElement, progress: number): boolean => { const duration = videoEl.duration; if (!Number.isFinite(duration) || duration <= 0) return false; 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 (Math.abs(videoEl.currentTime - targetTime) > 0.08) { try { videoEl.currentTime = targetTime; } catch { return false; } } return true; }, [], ); const tryPlayVideo = useCallback( (videoEl: HTMLVideoElement, imageId: string): void => { videoEl.play().catch((err: unknown) => { const errorName = err instanceof DOMException ? err.name : ""; if ( errorName === "NotAllowedError" || errorName === "NotSupportedError" ) { onAnimatedFailed(imageId); } }); }, [onAnimatedFailed], ); // 虚拟窗口:同时渲染前一张、当前张、后一张静态图以保证切页无白屏,同时仅当前张挂载单个