From b1afe5608d954fbe3cbf155c74d699636f09044e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=8F=E6=9D=B0?= Date: Wed, 19 Aug 2026 18:26:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=89=8D=E7=AB=AF=E8=87=AA=E4=B8=BB?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E5=88=87=E7=89=87=EF=BC=8C=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E5=90=8E=E7=AB=AF=E5=88=87=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - demuxSegment 中按 [startTime, endTime] 时间范围过滤 MP4 samples - 确保从关键帧开始提取,跳过非关键帧 - samples 存入 SegmentMeta,decodeSegment 直接使用,不再二次解析 - 移除 decodeSegment 中多余的 mp4box 重解析逻辑 - Fallback:时间范围内无样本时使用全部样本从首个关键帧开始 --- .../pages/generate/hooks/useCanvasPlayer.ts | 98 +++++++++++-------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts b/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts index f68f65cf2..cdd8f0314 100644 --- a/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts +++ b/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts @@ -136,6 +136,8 @@ interface SegmentMeta { videoHeight: number /** 解码器配置数据(HEVC hvcC / H.264 avcC),WebCodecs 必需 */ description?: ArrayBuffer + /** 前端提取的样本数据(已按时间范围过滤,从关键帧开始) */ + samples: Sample[] } // ── 播放器状态 ── @@ -255,7 +257,7 @@ export function useCanvasPlayer( return undefined }, []) - // ── 解封装单个片段,提取轨道元数据 ── + // ── 解封装单个片段,提取轨道元数据 + 按时间范围过滤样本 ── const demuxSegment = useCallback( (buffer: ArrayBuffer, segIndex: number): SegmentMeta | null => { const segment = segments[segIndex] @@ -280,7 +282,7 @@ export function useCanvasPlayer( // 提取编解码器配置数据(HEVC 必需,H.264 也需要) let description = extractCodecDescription(buffer) - // 如果当前分片没有 description,尝试从缓存获取(后续分片可能不含 moov box) + // 如果当前分片没有 description,尝试从缓存获取 if (!description) { for (const cached of descriptionCache.current.values()) { description = cached @@ -288,7 +290,7 @@ export function useCanvasPlayer( } } - // 如果找到了 description,缓存起来供后续分片使用 + // 缓存 description 供后续分片使用 if (description) { descriptionCache.current.set(segment.assetId, description) } @@ -304,20 +306,62 @@ export function useCanvasPlayer( videoWidth: videoTrack.track_width || 1280, videoHeight: videoTrack.track_height || 720, description, + samples: [], } - // 提取所有 samples(start() 会同步触发 onSamples) + // 提取所有 samples mp4File.setExtractionOptions(videoTrack.id, null, { nbSamples: videoTrack.nb_samples, }) mp4File.start() } + mp4File.onSamples = (_trackId: number, _user: unknown, samples: Sample[]) => { + if (!meta) return + + // 前端切片:按 [startTime, endTime] 时间范围过滤样本 + const timescale = meta.timescale + const startCts = segment.startTime * timescale + const endCts = segment.endTime * timescale + + // 过滤出时间范围内的样本 + let filtered = samples.filter((s) => s.cts >= startCts && s.cts < endCts) + + // 确保从关键帧开始(跳过第一个 sync 之前的非关键帧) + let foundSync = false + filtered = filtered.filter((s) => { + if (s.is_sync) { + foundSync = true + return true + } + return foundSync + }) + + // Fallback:如果时间范围内没有样本,使用全部样本从第一个关键帧开始 + if (filtered.length === 0) { + console.warn( + `[useCanvasPlayer] No samples in range [${segment.startTime}s, ${segment.endTime}s] for segment ${segIndex}, fallback to all from keyframe`, + ) + let sync = false + filtered = samples.filter((s) => { + if (s.is_sync) { + sync = true + return true + } + return sync + }) + } + + meta.samples = filtered + console.log( + `[useCanvasPlayer] Segment ${segIndex}: ${filtered.length}/${samples.length} samples (range ${segment.startTime}s-${segment.endTime}s)`, + ) + } + mp4File.onError = (_module: string, message: string) => { console.error(`[useCanvasPlayer] MP4Box error: ${message}`) } - // mp4box 需要在 buffer 上设置 fileStart 属性 // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(buffer as any).fileStart = 0 // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -330,10 +374,9 @@ export function useCanvasPlayer( // ── 初始化 VideoDecoder 并解码指定片段 ── const decodeSegment = useCallback( - async (buffer: ArrayBuffer, meta: SegmentMeta): Promise => { + async (_buffer: ArrayBuffer, meta: SegmentMeta): Promise => { if (isDestroyedRef.current) return - const mp4File = createFile() let decoderReady = false // 配置解码器(每个片段可能需要不同的 codec/分辨率) @@ -381,47 +424,18 @@ export function useCanvasPlayer( if (!decoderReady) return - // 收集 samples - const samplesCollected: Sample[] = [] - - mp4File.onReady = (info: Movie) => { - const videoTrack = info.videoTracks[0] - if (!videoTrack) return - - mp4File.setExtractionOptions(videoTrack.id, null, { - nbSamples: videoTrack.nb_samples, - }) - mp4File.start() + // 使用 demuxSegment 中已提取并过滤的 samples(前端切片) + const samplesCollected = meta.samples + if (samplesCollected.length === 0) { + console.warn("[useCanvasPlayer] No samples to decode for segment", meta.assetId) + return } - mp4File.onSamples = (_trackId: number, _user: unknown, samples: Sample[]) => { - samplesCollected.push(...samples) - } - - mp4File.onError = (_module: string, message: string) => { - console.error(`[useCanvasPlayer] MP4Box decode error: ${message}`) - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(buffer as any).fileStart = 0 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mp4File.appendBuffer(buffer as any) - - // 等待 samples 收集完成(mp4box start() 是同步的) - await new Promise((resolve) => setTimeout(resolve, 10)) - - // 送入解码器(跳过第一个关键帧之前的非关键帧) - let hasKeyFrame = false + // 送入解码器 for (const sample of samplesCollected) { if (!sample.data || isDestroyedRef.current) continue if (decoder.state === "closed") break - if (!hasKeyFrame && !sample.is_sync) { - console.warn("[useCanvasPlayer] Skipping non-keyframe chunk before first sync") - continue - } - if (sample.is_sync) hasKeyFrame = true - const chunk = new EncodedVideoChunk({ type: sample.is_sync ? "key" : "delta", timestamp: (sample.cts / meta.timescale) * 1_000_000, -- 2.54.0