fix: 修复RAF循环穿透导致指数级增长+音频同步重置卡顿
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m4s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m20s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m54s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m0s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m3s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m6s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m14s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m41s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m30s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m56s
AI Code Review / AI Code Review (pull_request) Successful in 5m47s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m52s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m18s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m23s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 56s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m4s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m21s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m8s
CI/CD Pipeline / Integration Tests (push) Successful in 2m59s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / Unit Tests (push) Successful in 13m23s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m3s
CI/CD Pipeline / CI Gate (pull_request) Successful in 8s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 601h33m2s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 601h33m2s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 601h33m4s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 601h33m4s
CI/CD Pipeline / Production Browser E2E (push) Failing after 601h33m18s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 601h33m4s
CI/CD Pipeline / Deploy Production (push) Failing after 601h33m21s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 601h33m23s
CI/CD Pipeline / Build Production Web Image (push) Failing after 601h33m24s
CI/CD Pipeline / CI Gate (push) Failing after 601h33m24s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 601h44m23s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 601h44m27s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 601h44m29s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 601h46m1s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 601h46m2s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 601h46m3s
CI/CD Pipeline / PR Build API Image (push) Failing after 601h46m7s
CI/CD Pipeline / Frontend Lint (push) Failing after 601h46m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 601h46m59s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 601h47m1s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 601h47m5s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 601h48m10s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 602h6m50s
CI/CD Pipeline / Canary Release to Production (push) Failing after 602h7m8s
CI/CD Pipeline / Build Production API Image (push) Failing after 602h7m11s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 602h18m12s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 602h18m32s
CI/CD Pipeline / PR Build Web Image (push) Failing after 602h19m52s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 602h20m50s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m4s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m20s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m54s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m0s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m3s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m6s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m14s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m41s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m30s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m56s
AI Code Review / AI Code Review (pull_request) Successful in 5m47s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m52s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m18s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m23s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 56s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m4s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m21s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m8s
CI/CD Pipeline / Integration Tests (push) Successful in 2m59s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / Unit Tests (push) Successful in 13m23s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m3s
CI/CD Pipeline / CI Gate (pull_request) Successful in 8s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 601h33m2s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 601h33m2s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 601h33m4s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 601h33m4s
CI/CD Pipeline / Production Browser E2E (push) Failing after 601h33m18s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 601h33m4s
CI/CD Pipeline / Deploy Production (push) Failing after 601h33m21s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 601h33m23s
CI/CD Pipeline / Build Production Web Image (push) Failing after 601h33m24s
CI/CD Pipeline / CI Gate (push) Failing after 601h33m24s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 601h44m23s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 601h44m27s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 601h44m29s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 601h46m1s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 601h46m2s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 601h46m3s
CI/CD Pipeline / PR Build API Image (push) Failing after 601h46m7s
CI/CD Pipeline / Frontend Lint (push) Failing after 601h46m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 601h46m59s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 601h47m1s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 601h47m5s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 601h48m10s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 602h6m50s
CI/CD Pipeline / Canary Release to Production (push) Failing after 602h7m8s
CI/CD Pipeline / Build Production API Image (push) Failing after 602h7m11s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 602h18m12s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 602h18m32s
CI/CD Pipeline / PR Build Web Image (push) Failing after 602h19m52s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 602h20m50s
useSegmentScheduler.ts: - 新增 tickRef 保存最新 tick 引用,所有 RAF 调度通过 () => tickRef.current() - 片段边界切换分支加 return,阻止旧闭包穿透到末尾重复调度 RAF (这是网页越来越卡的根因:每次片段切换 RAF 链数量翻倍) - play() 和重播分支的 RAF 同样改用 tickRef FrontendPreviewPlayer.tsx: - 片段切换音频同步 useEffect 移除 currentTime 依赖 原依赖每200ms变化导致 audio.currentTime 被反复重置,音频卡顿
This commit is contained in:
@@ -198,7 +198,9 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
|
||||
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) => {
|
||||
|
||||
@@ -98,6 +98,7 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
|
||||
const rafRef = useRef<number>(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 {
|
||||
|
||||
Reference in New Issue
Block a user