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
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user