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)