From e3e57d236a463903fca5adf4a4b1876e34ca0086 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 20 Aug 2026 11:44:32 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20AI=20Code=20Review?= =?UTF-8?q?=20v4=EF=BC=88VideoFrame=20=E6=89=80=E6=9C=89=E6=9D=83+?= =?UTF-8?q?=E8=A7=A3=E7=A0=81=E5=B9=B6=E5=8F=91=E7=AB=9E=E6=80=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 lastDrawnFrameRef:getCurrentFrame 返回 FrameQueue 内部引用,帧生命周期由 FrameQueue 管理(push 淘汰/close 时释放),渲染层不应 close - 引入 decodeGenerationRef 计数器:seek/destroy 时递增,decodeAroundPosition 在 await 前后检查代数是否匹配,避免并发竞态 - 初始化解码循环同样加入 generation 检查 --- .../pages/generate/hooks/useCanvasPlayer.ts | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts b/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts index d927a2621..9c260776a 100644 --- a/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts +++ b/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts @@ -220,8 +220,8 @@ export function useCanvasPlayer( const frameQueueRef = useRef(new FrameQueue(10)) /** 已解码的片段索引集合,用于按需解码(先标记防重入,失败时移除允许重试) */ const decodedSegmentsRef = useRef(new Set()) - /** 上一帧引用,绘制新帧前释放防止 VideoFrame 内存泄漏 */ - const lastDrawnFrameRef = useRef(null) + /** 解码代数计数器,seek 时递增以作废正在进行的异步解码 */ + const decodeGenerationRef = useRef(0) const rafRef = useRef(0) const playStartRef = useRef(0) const playStartOffsetRef = useRef(0) @@ -565,6 +565,9 @@ export function useCanvasPlayer( const metas = segmentMetaRef.current if (!metas || metas.length === 0) return + // 记录当前代数,seek 后代数变化则中止 + const gen = decodeGenerationRef.current + let targetIdx = -1 let acc = 0 for (let i = 0; i < metas.length; i++) { @@ -582,6 +585,8 @@ export function useCanvasPlayer( i <= Math.min(metas.length - 1, targetIdx + 1); i++ ) { + // seek 已作废当前解码任务 + if (decodeGenerationRef.current !== gen) return if (decodedSegmentsRef.current.has(i)) continue const meta = metas[i] const buffer = segmentDataRef.current.get(meta.assetId) @@ -595,6 +600,8 @@ export function useCanvasPlayer( decodedSegmentsRef.current.delete(i) console.warn(`[useCanvasPlayer] 按需解码片段 ${i} 失败:`, e) } + // await 后再次检查代数,seek 期间不更新标记 + if (decodeGenerationRef.current !== gen) return } }, [decodeSegment], @@ -701,20 +708,15 @@ export function useCanvasPlayer( const elapsed = (performance.now() - playStartRef.current) / 1000 const currentTime = Math.min(playStartOffsetRef.current + elapsed, totalDuration) + // getCurrentFrame 返回 FrameQueue 内部引用,帧生命周期由 FrameQueue 管理 + // (push 淘汰旧帧时 close、clear 时全部 close),渲染层不应 close const frame = frameQueueRef.current.getCurrentFrame(currentTime) - // 释放上一帧 VideoFrame,防止内存泄漏 - if (lastDrawnFrameRef.current) { - lastDrawnFrameRef.current.close() - lastDrawnFrameRef.current = null - } - ctx.clearRect(0, 0, canvas.width, canvas.height) if (frame) { const rect = computeDrawRect(canvas.width, canvas.height) ctx.drawImage(frame, rect.dx, rect.dy, rect.dw, rect.dh) - lastDrawnFrameRef.current = frame } if (titleSettings?.text) { @@ -765,13 +767,10 @@ export function useCanvasPlayer( setState((s) => ({ ...s, currentTime: clampedTime })) playStartOffsetRef.current = clampedTime playStartRef.current = performance.now() - // seek 后清空帧队列 + 释放上一帧 + // seek 时递增解码代数,作废正在进行的异步解码 + decodeGenerationRef.current++ + // 清空帧队列(clear 内部会 close 所有帧)+ 清空已解码标记 frameQueueRef.current.clear() - if (lastDrawnFrameRef.current) { - lastDrawnFrameRef.current.close() - lastDrawnFrameRef.current = null - } - // 清空已解码标记,重新解码 seek 目标区域 decodedSegmentsRef.current.clear() await decodeAroundPosition(clampedTime) }, @@ -786,12 +785,8 @@ export function useCanvasPlayer( decoderRef.current.close() } - // 释放上一帧引用 - if (lastDrawnFrameRef.current) { - lastDrawnFrameRef.current.close() - lastDrawnFrameRef.current = null - } - + // 递增代数中止进行中的异步解码,清空帧队列(clear 内部 close 所有帧) + decodeGenerationRef.current++ frameQueueRef.current.clear() segmentDataRef.current.clear() segmentMetaRef.current = [] @@ -874,9 +869,12 @@ export function useCanvasPlayer( // 4. 按需解码:初始只解码前 3 个片段,后续通过 decodeAroundPosition 动态加载 // 避免一次性全量解码导致 frameQueue 环形缓冲区旧帧被丢弃引发黑屏 decodedSegmentsRef.current.clear() + const initGen = decodeGenerationRef.current const initialDecodeCount = Math.min(metas.length, 3) for (let i = 0; i < initialDecodeCount; i++) { if (cancelled) break + // seek 或 destroy 已作废当前初始化 + if (decodeGenerationRef.current !== initGen) break const meta = metas[i] const buffer = segmentDataRef.current.get(meta.assetId) if (!buffer) continue