fix: 修复 AI Code Review v4(VideoFrame 所有权+解码并发竞态)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 50s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 50s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m51s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m58s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m25s
AI Code Review / AI Code Review (pull_request) Failing after 2m26s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m36s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m12s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m16s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m34s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 651h40m1s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 651h40m52s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 651h41m38s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 651h41m40s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 651h41m42s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 651h41m44s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 651h41m44s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 651h41m45s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 652h14m36s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 652h15m26s

- 移除 lastDrawnFrameRef:getCurrentFrame 返回 FrameQueue 内部引用,帧生命周期由 FrameQueue 管理(push 淘汰/close 时释放),渲染层不应 close
- 引入 decodeGenerationRef 计数器:seek/destroy 时递增,decodeAroundPosition 在 await 前后检查代数是否匹配,避免并发竞态
- 初始化解码循环同样加入 generation 检查
This commit is contained in:
xiaoxia
2026-08-20 11:44:32 +08:00
parent f729524ad0
commit e3e57d236a
@@ -220,8 +220,8 @@ export function useCanvasPlayer(
const frameQueueRef = useRef(new FrameQueue(10))
/** 已解码的片段索引集合,用于按需解码(先标记防重入,失败时移除允许重试) */
const decodedSegmentsRef = useRef(new Set<number>())
/** 上一帧引用,绘制新帧前释放防止 VideoFrame 内存泄漏 */
const lastDrawnFrameRef = useRef<VideoFrame | null>(null)
/** 解码代数计数器,seek 时递增以作废正在进行的异步解码 */
const decodeGenerationRef = useRef(0)
const rafRef = useRef<number>(0)
const playStartRef = useRef<number>(0)
const playStartOffsetRef = useRef<number>(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