diff --git a/apps/web/src/pages/generate/components/FrontendPreviewPlayer.tsx b/apps/web/src/pages/generate/components/FrontendPreviewPlayer.tsx index c38058815..3637af79c 100644 --- a/apps/web/src/pages/generate/components/FrontendPreviewPlayer.tsx +++ b/apps/web/src/pages/generate/components/FrontendPreviewPlayer.tsx @@ -198,7 +198,9 @@ const FrontendPreviewPlayer: React.FC = ({ const audio = audioRef.current if (!audio || !audio.src || !isPlaying) return audio.currentTime = currentTime - }, [segmentSyncKey, isPlaying, currentTime]) + // 注意:不要把 currentTime 放进依赖数组,否则每200ms会重置音频位置导致卡顿 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [segmentSyncKey, isPlaying]) const handleSeekTo = useCallback( (time: number) => { diff --git a/apps/web/src/pages/generate/hooks/useSegmentScheduler.ts b/apps/web/src/pages/generate/hooks/useSegmentScheduler.ts index 31e5b7540..d7ab7cc94 100644 --- a/apps/web/src/pages/generate/hooks/useSegmentScheduler.ts +++ b/apps/web/src/pages/generate/hooks/useSegmentScheduler.ts @@ -98,6 +98,7 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul const rafRef = useRef(0) const isSeekingRef = useRef(false) const lastTimeUpdateRef = useRef(0) + const tickRef = useRef<() => void>(() => {}) // 计算时间线 const timelineStarts = useMemo(() => buildTimeline(segments), [segments]) @@ -178,14 +179,15 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul const tick = useCallback(() => { const video = videoRefs.current[currentSegmentIndex] if (!video || isSeekingRef.current) { - rafRef.current = requestAnimationFrame(tick) + tickRef.current = tick + rafRef.current = requestAnimationFrame(() => tickRef.current()) return } const seg = segments[currentSegmentIndex] if (!seg) return - // 预加载下一个片段:距离出点 3 秒时,提前设置下一个视频的 currentTime 触发缓冲 + // 预加载下一个片段:距离出点 3 秒时,提前 seek 下一个视频触发缓冲 const nextIndex = currentSegmentIndex + 1 if (nextIndex < segments.length) { const nextVideo = videoRefs.current[nextIndex] @@ -201,14 +203,14 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul // 检查是否到达出点(容差 0.15s) if (video.currentTime >= seg.endTime - 0.15) { video.pause() - const nextIndex = currentSegmentIndex + 1 if (nextIndex < segments.length) { + // ⚠️ 必须 return,不能继续走到下面的 requestAnimationFrame! + // 否则旧 tick 闭包会继续调度自己,导致 RAF 循环指数级增长 switchToSegment(nextIndex).then(() => { setIsPlaying(true) - rafRef.current = requestAnimationFrame(tick) const nextVideo = videoRefs.current[nextIndex] if (nextVideo) { - const canPlay = () => { + const startNext = () => { nextVideo .play() .catch((e) => @@ -216,42 +218,46 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul ) } if (nextVideo.readyState >= 3) { - canPlay() + startNext() } else { - const timeout = setTimeout(canPlay, 300) + const timeout = setTimeout(startNext, 300) nextVideo.addEventListener( "canplay", () => { clearTimeout(timeout) - canPlay() + startNext() }, { once: true }, ) } } + // 用 tickRef 调度最新的 tick(闭包已更新为 nextIndex) + rafRef.current = requestAnimationFrame(() => tickRef.current()) }) const accumulatedTime = (timelineStarts[currentSegmentIndex] || 0) + (seg.endTime - seg.startTime) lastTimeUpdateRef.current = 0 setCurrentTime(accumulatedTime) + return // ✅ 关键:阻止穿透到下面的 RAF } else { setIsPlaying(false) setIsEnded(true) setCurrentTime(totalDuration) return } - } else { - const globalTime = - (timelineStarts[currentSegmentIndex] || 0) + (video.currentTime - seg.startTime) - // 节流:200ms 更新一次进度状态(5fps),避免 60fps re-render 拖慢页面 - const now = performance.now() - if (now - lastTimeUpdateRef.current >= 200) { - lastTimeUpdateRef.current = now - setCurrentTime(Math.max(0, Math.min(globalTime, totalDuration))) - } } - rafRef.current = requestAnimationFrame(tick) + const globalTime = + (timelineStarts[currentSegmentIndex] || 0) + (video.currentTime - seg.startTime) + const now = performance.now() + if (now - lastTimeUpdateRef.current >= 200) { + lastTimeUpdateRef.current = now + setCurrentTime(Math.max(0, Math.min(globalTime, totalDuration))) + } + + // 用 tickRef 保持 RAF 链始终调用最新版本 + tickRef.current = tick + rafRef.current = requestAnimationFrame(() => tickRef.current()) }, [segments, currentSegmentIndex, timelineStarts, totalDuration, switchToSegment]) /** 播放 */ @@ -275,7 +281,8 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul await playPromise } setIsPlaying(true) - rafRef.current = requestAnimationFrame(tick) + tickRef.current = tick + rafRef.current = requestAnimationFrame(() => tickRef.current()) } catch (err) { console.warn("[useSegmentScheduler] 播放失败:", err) } @@ -304,7 +311,8 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul video.play().catch((e) => console.warn("[useSegmentScheduler] restart play failed:", e)) setIsPlaying(true) setCurrentTime(0) - rafRef.current = requestAnimationFrame(tick) + tickRef.current = tick + rafRef.current = requestAnimationFrame(() => tickRef.current()) } }) } else {