fix: 第9轮最终简化方案 — init全量解码+播放时只渲染
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m21s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m27s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m5s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m8s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m14s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m17s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m22s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 53s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m0s
AI Code Review / AI Code Review (pull_request) Failing after 3m45s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m33s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m6s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m36s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 57s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m1s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m2s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m51s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m10s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m18s
CI/CD Pipeline / Unit Tests (push) Successful in 14m31s
CI/CD Pipeline / Integration Tests (push) Successful in 5m2s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m48s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m14s
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 616h54m24s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 616h54m26s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 616h54m25s
CI/CD Pipeline / CI Gate (push) Failing after 616h54m55s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 616h54m25s
CI/CD Pipeline / Deploy Production (push) Failing after 616h55m16s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 616h54m26s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 616h55m17s
CI/CD Pipeline / Production Browser E2E (push) Failing after 616h55m14s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 617h5m29s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 617h5m31s
CI/CD Pipeline / Build Production Web Image (push) Failing after 616h55m17s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 617h6m37s
CI/CD Pipeline / Build Production API Image (push) Failing after 616h55m18s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 617h6m46s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 617h5m31s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 617h8m18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 617h5m33s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 617h8m22s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 617h6m28s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 617h9m47s
CI/CD Pipeline / Frontend Lint (push) Failing after 617h9m51s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 617h9m52s
CI/CD Pipeline / PR Build Web Image (push) Failing after 617h9m49s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 617h28m13s
CI/CD Pipeline / Canary Release to Production (push) Failing after 617h29m2s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 617h40m22s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 617h42m7s
CI/CD Pipeline / PR Build API Image (push) Failing after 617h43m37s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m21s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m27s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m5s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m8s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m14s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m17s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m22s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 53s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m0s
AI Code Review / AI Code Review (pull_request) Failing after 3m45s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m33s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m6s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m36s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 57s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m1s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m2s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m51s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m10s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m18s
CI/CD Pipeline / Unit Tests (push) Successful in 14m31s
CI/CD Pipeline / Integration Tests (push) Successful in 5m2s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m48s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m14s
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 616h54m24s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 616h54m26s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 616h54m25s
CI/CD Pipeline / CI Gate (push) Failing after 616h54m55s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 616h54m25s
CI/CD Pipeline / Deploy Production (push) Failing after 616h55m16s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 616h54m26s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 616h55m17s
CI/CD Pipeline / Production Browser E2E (push) Failing after 616h55m14s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 617h5m29s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 617h5m31s
CI/CD Pipeline / Build Production Web Image (push) Failing after 616h55m17s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 617h6m37s
CI/CD Pipeline / Build Production API Image (push) Failing after 616h55m18s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 617h6m46s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 617h5m31s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 617h8m18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 617h5m33s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 617h8m22s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 617h6m28s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 617h9m47s
CI/CD Pipeline / Frontend Lint (push) Failing after 617h9m51s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 617h9m52s
CI/CD Pipeline / PR Build Web Image (push) Failing after 617h9m49s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 617h28m13s
CI/CD Pipeline / Canary Release to Production (push) Failing after 617h29m2s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 617h40m22s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 617h42m7s
CI/CD Pipeline / PR Build API Image (push) Failing after 617h43m37s
根因:之前 init 只解 60 帧,播放中续解码导致关键帧错误、 重复帧、队列混乱、抖动。 新方案: - init 阶段一次性解码全部 894 帧(队列 1024 足够容纳) - 播放时只做 drawImage,不触发任何解码 - 重播直接复用已有帧,不重新解码 - seek 直接定位,不清帧队列 - 删除 decodeAroundPosition 动态解码函数
This commit is contained in:
@@ -9,8 +9,6 @@ import { createFile } from "mp4box"
|
||||
import type { Movie, Sample } from "mp4box"
|
||||
|
||||
// ── 常量 ──
|
||||
/** 初始化预解码最大帧数(约 2 秒 @30fps),后续帧通过 decodeAroundPosition 按需解码 */
|
||||
const MAX_INIT_FRAMES = 60
|
||||
|
||||
/**
|
||||
* 规范化 mp4box 提取的 codec 字符串为 WebCodecs 兼容格式
|
||||
@@ -666,77 +664,6 @@ export function useCanvasPlayer(
|
||||
[],
|
||||
)
|
||||
|
||||
/**
|
||||
* 按需解码当前播放位置 ±1 个片段。
|
||||
* 在渲染循环中定期调用,避免一次性解码所有片段导致环形缓冲区溢出丢帧。
|
||||
* 使用"先标记再解码"模式防止并发重复解码,失败时移除标记允许重试。
|
||||
*/
|
||||
const decodeAroundPosition = useCallback(
|
||||
async (currentTime: number) => {
|
||||
const metas = segmentMetaRef.current
|
||||
if (!metas || metas.length === 0) return
|
||||
|
||||
const gen = decodeGenerationRef.current
|
||||
|
||||
let targetIdx = -1
|
||||
let acc = 0
|
||||
for (let i = 0; i < metas.length; i++) {
|
||||
const dur = metas[i].globalEndTime - metas[i].globalStartTime
|
||||
if (currentTime < acc + dur) {
|
||||
targetIdx = i
|
||||
break
|
||||
}
|
||||
acc += dur
|
||||
}
|
||||
if (targetIdx === -1) targetIdx = metas.length - 1
|
||||
|
||||
// 向前看 2 个片段,提前预解码避免切换卡顿
|
||||
const endIdx = Math.min(metas.length - 1, targetIdx + 2)
|
||||
for (let i = Math.max(0, targetIdx - 1); i <= endIdx; i++) {
|
||||
if (decodeGenerationRef.current !== gen) return
|
||||
const meta = metas[i]
|
||||
const buffer = segmentDataRef.current.get(meta.assetId)
|
||||
if (!buffer) continue
|
||||
|
||||
const frameCount = decodedFrameCountsRef.current.get(i) ?? 0
|
||||
const totalSamples = meta.samples.length
|
||||
|
||||
// 完全解码的片段:检查队列深度
|
||||
if (frameCount === Infinity) {
|
||||
const availableFrames = frameQueueRef.current.size
|
||||
if (availableFrames > 90) continue
|
||||
// 队列不足,强制重新解码
|
||||
decodedFrameCountsRef.current.delete(i)
|
||||
decodedSegmentsRef.current.delete(i)
|
||||
}
|
||||
|
||||
// 已标记为解码中但未完成:续解码
|
||||
if (decodedSegmentsRef.current.has(i)) {
|
||||
const currentCount = decodedFrameCountsRef.current.get(i) ?? 0
|
||||
if (currentCount >= totalSamples) continue
|
||||
try {
|
||||
await decodeSegment(buffer, meta, 300)
|
||||
} catch (e) {
|
||||
console.warn(`[useCanvasPlayer] 续解码片段 ${i} 失败:`, e)
|
||||
}
|
||||
if (decodeGenerationRef.current !== gen) return
|
||||
continue
|
||||
}
|
||||
|
||||
// 未开始解码的片段:开始解码
|
||||
decodedSegmentsRef.current.add(i)
|
||||
try {
|
||||
await decodeSegment(buffer, meta, 300)
|
||||
} catch (e) {
|
||||
decodedSegmentsRef.current.delete(i)
|
||||
console.warn(`[useCanvasPlayer] 按需解码片段 ${i} 失败:`, e)
|
||||
}
|
||||
if (decodeGenerationRef.current !== gen) return
|
||||
}
|
||||
},
|
||||
[decodeSegment],
|
||||
)
|
||||
|
||||
// ── 标题绘制 ──
|
||||
const drawTitle = useCallback(
|
||||
(
|
||||
@@ -863,8 +790,6 @@ export function useCanvasPlayer(
|
||||
}
|
||||
return s
|
||||
})
|
||||
// 按需解码当前 ±1 片段
|
||||
decodeAroundPosition(currentTime)
|
||||
}
|
||||
|
||||
if (currentTime >= totalDuration) {
|
||||
@@ -873,7 +798,7 @@ export function useCanvasPlayer(
|
||||
}
|
||||
|
||||
rafRef.current = requestAnimationFrame(renderFrame)
|
||||
}, [canvasRef, totalDuration, titleSettings, drawTitle, computeDrawRect, decodeAroundPosition])
|
||||
}, [canvasRef, totalDuration, titleSettings, drawTitle, computeDrawRect])
|
||||
|
||||
// ── 播放控制 ──
|
||||
const play = useCallback(async () => {
|
||||
@@ -881,11 +806,7 @@ export function useCanvasPlayer(
|
||||
|
||||
// 播放结束后重播 或 从起点重新播放:重置到起点,清空解码缓存
|
||||
if (state.currentTime >= totalDuration - 0.1 || state.currentTime <= 0.1) {
|
||||
decodeGenerationRef.current++
|
||||
// 清空帧计数:让 decodeAroundPosition 认为所有片段都需要解码
|
||||
decodedFrameCountsRef.current.clear()
|
||||
decodedSegmentsRef.current.clear()
|
||||
// 不清空帧队列:旧帧仍有效(按绝对时间戳索引),getCurrentFrame 可继续使用
|
||||
// 重播:帧已在 init 阶段全部解码并按绝对时间戳存入队列,直接复用
|
||||
playStartOffsetRef.current = 0
|
||||
setState((s) => ({ ...s, currentTime: 0 }))
|
||||
}
|
||||
@@ -899,9 +820,7 @@ export function useCanvasPlayer(
|
||||
}
|
||||
lastProgressUpdateRef.current = 0
|
||||
rafRef.current = requestAnimationFrame(renderFrame)
|
||||
// 立即触发一次按需解码,不等渲染循环 200ms 节流
|
||||
decodeAroundPosition(state.currentTime)
|
||||
}, [state.hasSupport, state.currentTime, renderFrame, decodeAroundPosition])
|
||||
}, [state.hasSupport, state.currentTime, totalDuration, renderFrame])
|
||||
|
||||
const pause = useCallback(() => {
|
||||
setState((s) => ({ ...s, isPlaying: false }))
|
||||
@@ -914,15 +833,9 @@ export function useCanvasPlayer(
|
||||
setState((s) => ({ ...s, currentTime: clampedTime }))
|
||||
playStartOffsetRef.current = clampedTime
|
||||
playStartRef.current = performance.now()
|
||||
// seek 时递增解码代数,作废正在进行的异步解码
|
||||
decodeGenerationRef.current++
|
||||
// 清空帧队列(clear 内部会 close 所有帧)+ 清空已解码标记
|
||||
frameQueueRef.current.clear()
|
||||
decodedSegmentsRef.current.clear()
|
||||
decodedFrameCountsRef.current.clear()
|
||||
await decodeAroundPosition(clampedTime)
|
||||
// init 阶段已全量解码,seek 直接定位到对应帧即可
|
||||
},
|
||||
[totalDuration, decodeAroundPosition],
|
||||
[totalDuration],
|
||||
)
|
||||
|
||||
const destroy = useCallback(() => {
|
||||
@@ -1056,7 +969,7 @@ export function useCanvasPlayer(
|
||||
decodedSegmentsRef.current.add(i)
|
||||
console.log(`[DIAG_v2] Calling decodeSegment(${meta.assetId})...`)
|
||||
try {
|
||||
await decodeSegment(buffer, meta, MAX_INIT_FRAMES)
|
||||
await decodeSegment(buffer, meta)
|
||||
console.log(`[DIAG_v2] decodeSegment(${meta.assetId}) returned OK`)
|
||||
} catch (e) {
|
||||
// 解码失败则移除标记,允许后续重试
|
||||
|
||||
Reference in New Issue
Block a user