From 8b30193f3e8d951c79154baf13afdd33304d919f Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 20 Aug 2026 19:21:32 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Canvas=20=E9=A2=84=E8=A7=88=E9=BB=91?= =?UTF-8?q?=E5=B1=8F=E4=BF=AE=E5=A4=8D=20-=20=E5=A2=9E=E5=A4=A7=E5=B8=A7?= =?UTF-8?q?=E7=BC=93=E5=86=B2=20+=20=E9=99=90=E5=88=B6=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E9=A2=84=E8=A7=A3=E7=A0=81=20+=20play=20=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E6=B8=85=E7=A9=BA=E5=B8=A7=E9=98=9F=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FrameQueue maxSize 从 10 改为 60(约 2 秒 @30fps),给按需解码留出足够预缓冲空间 - 新增 MAX_INIT_FRAMES=60 常量,decodeSegment 支持 maxFrames 参数,初始化阶段只解码前 60 帧避免帧挤兑 - play() 重播时同步清空 frameQueue(之前只清 decodedSegments 标记没清帧缓冲) - play() 启动渲染循环后立即触发 decodeAroundPosition,不等 200ms 节流延迟 --- .../pages/generate/hooks/useCanvasPlayer.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts b/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts index 1eff4e5f5..00afb22b6 100644 --- a/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts +++ b/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts @@ -8,6 +8,10 @@ import { useRef, useCallback, useEffect, useState } from "react" import { createFile } from "mp4box" import type { Movie, Sample } from "mp4box" +// ── 常量 ── +/** 初始化预解码最大帧数(约 2 秒 @30fps),后续帧通过 decodeAroundPosition 按需解码 */ +const MAX_INIT_FRAMES = 60 + // ── MP4 Box 解析辅助函数 ── // MP4 标准容器 box 列表(递归时会进入这些 box 内部搜索子 box) @@ -217,7 +221,7 @@ export function useCanvasPlayer( // ── 内部引用 ── const decoderRef = useRef(null) - const frameQueueRef = useRef(new FrameQueue(10)) + const frameQueueRef = useRef(new FrameQueue(60)) /** 已解码的片段索引集合,用于按需解码(先标记防重入,失败时移除允许重试) */ const decodedSegmentsRef = useRef(new Set()) /** 解码代数计数器,seek 时递增以作废正在进行的异步解码 */ @@ -449,7 +453,7 @@ export function useCanvasPlayer( // ── 初始化 VideoDecoder 并解码指定片段 ── const decodeSegment = useCallback( - async (_buffer: ArrayBuffer, meta: SegmentMeta): Promise => { + async (_buffer: ArrayBuffer, meta: SegmentMeta, maxFrames?: number): Promise => { if (isDestroyedRef.current) return let decoderReady = false @@ -521,6 +525,13 @@ export function useCanvasPlayer( continue } if (decoder.state === "closed") break + // 初始化阶段限制解码帧数,避免帧缓冲溢出 + if (maxFrames && decodedCount >= maxFrames) { + console.log( + `[useCanvasPlayer] Segment ${meta.assetId}: init decode limited to ${maxFrames} frames`, + ) + break + } const chunk = new EncodedVideoChunk({ type: sample.is_sync ? "key" : "delta", @@ -758,6 +769,8 @@ export function useCanvasPlayer( if (state.currentTime <= 0.1 && decodedSegmentsRef.current.size > 0) { decodeGenerationRef.current++ decodedSegmentsRef.current.clear() + // 同步清空帧缓冲,避免旧帧残留导致 getCurrentFrame 返回 null + frameQueueRef.current.clear() } setState((s) => ({ ...s, isPlaying: true })) @@ -765,7 +778,9 @@ export function useCanvasPlayer( playStartOffsetRef.current = state.currentTime lastProgressUpdateRef.current = 0 rafRef.current = requestAnimationFrame(renderFrame) - }, [state.hasSupport, state.currentTime, renderFrame]) + // 立即触发一次按需解码,不等渲染循环 200ms 节流 + decodeAroundPosition(state.currentTime) + }, [state.hasSupport, state.currentTime, renderFrame, decodeAroundPosition]) const pause = useCallback(() => { setState((s) => ({ ...s, isPlaying: false })) @@ -887,7 +902,7 @@ export function useCanvasPlayer( // 先标记为解码中,防止重复解码 decodedSegmentsRef.current.add(i) try { - await decodeSegment(buffer, meta) + await decodeSegment(buffer, meta, MAX_INIT_FRAMES) } catch (e) { // 解码失败则移除标记,允许后续重试 decodedSegmentsRef.current.delete(i)