From 18371dc4384258b7e604b2b1a26da53c7d6c0fd8 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sat, 22 Aug 2026 13:38:02 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DRAF=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E7=A9=BF=E9=80=8F=E5=AF=BC=E8=87=B4=E6=8C=87=E6=95=B0=E7=BA=A7?= =?UTF-8?q?=E5=A2=9E=E9=95=BF+=E9=9F=B3=E9=A2=91=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E9=87=8D=E7=BD=AE=E5=8D=A1=E9=A1=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useSegmentScheduler.ts: - 新增 tickRef 保存最新 tick 引用,所有 RAF 调度通过 () => tickRef.current() - 片段边界切换分支加 return,阻止旧闭包穿透到末尾重复调度 RAF (这是网页越来越卡的根因:每次片段切换 RAF 链数量翻倍) - play() 和重播分支的 RAF 同样改用 tickRef FrontendPreviewPlayer.tsx: - 片段切换音频同步 useEffect 移除 currentTime 依赖 原依赖每200ms变化导致 audio.currentTime 被反复重置,音频卡顿 --- .../components/FrontendPreviewPlayer.tsx | 4 +- .../generate/hooks/useSegmentScheduler.ts | 48 +++++++++++-------- 2 files changed, 31 insertions(+), 21 deletions(-) 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 {