diff --git a/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts b/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts index a859f61a9..e5acdd92d 100644 --- a/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts +++ b/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts @@ -258,8 +258,9 @@ export function useCanvasPlayer( }, []) // ── 解封装单个片段,提取轨道元数据 + 按时间范围过滤样本 ── + // ✅ 关键修复:改为异步函数,等待 MP4Box.js 的 onSamples 回调完成后再返回 const demuxSegment = useCallback( - (buffer: ArrayBuffer, segIndex: number): SegmentMeta | null => { + async (buffer: ArrayBuffer, segIndex: number): Promise => { const segment = segments?.[segIndex] if (!segment) { console.warn("[useCanvasPlayer] No segment at index", segIndex) @@ -273,104 +274,113 @@ export function useCanvasPlayer( } const mp4File = createFile() - let meta: SegmentMeta | null = null - mp4File.onReady = (info: Movie) => { - const videoTrack = info?.videoTracks?.[0] - if (!videoTrack) { - console.warn("[useCanvasPlayer] No video track found for segment", segIndex) - return - } + return new Promise((resolve) => { + let meta: SegmentMeta | null = null - // 提取编解码器配置数据(HEVC 必需,H.264 也需要) - let description = extractCodecDescription(buffer) - - // 如果当前分片没有 description,尝试从缓存获取 - if (!description) { - for (const cached of descriptionCache.current.values()) { - description = cached - break + mp4File.onReady = (info: Movie) => { + const videoTrack = info?.videoTracks?.[0] + if (!videoTrack) { + console.warn("[useCanvasPlayer] No video track found for segment", segIndex) + resolve(null) + return } - } - // 缓存 description 供后续分片使用 - if (description) { - descriptionCache.current.set(segment.assetId, description) - } + // 提取编解码器配置数据(HEVC 必需,H.264 也需要) + let description = extractCodecDescription(buffer) - meta = { - assetId: segment.assetId, - videoUrl: segment.videoUrl, - globalStartTime: globalStart, - globalEndTime: globalStart + (segment.endTime - segment.startTime), - trackId: videoTrack.id ?? 1, - timescale: videoTrack.timescale ?? 90000, - codec: videoTrack.codec ?? "avc1.42E01E", - videoWidth: videoTrack.track_width || 1280, - videoHeight: videoTrack.track_height || 720, - description, - samples: [], - } - - // 提取所有 samples - mp4File.setExtractionOptions(videoTrack.id ?? 1, null, { - nbSamples: videoTrack.nb_samples ?? 0, - }) - 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 ?? 0) >= startCts && (s?.cts ?? 0) < endCts) - - // 确保从关键帧开始(跳过第一个 sync 之前的非关键帧) - let foundSync = false - filtered = filtered.filter((s) => { - if (s.is_sync) { - foundSync = true - return true + // 如果当前分片没有 description,尝试从缓存获取 + if (!description) { + for (const cached of descriptionCache.current.values()) { + description = cached + break + } } - 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) => { + // 缓存 description 供后续分片使用 + if (description) { + descriptionCache.current.set(segment.assetId, description) + } + + meta = { + assetId: segment.assetId, + videoUrl: segment.videoUrl, + globalStartTime: globalStart, + globalEndTime: globalStart + (segment.endTime - segment.startTime), + trackId: videoTrack.id ?? 1, + timescale: videoTrack.timescale ?? 90000, + codec: videoTrack.codec ?? "avc1.42E01E", + videoWidth: videoTrack.track_width || 1280, + videoHeight: videoTrack.track_height || 720, + description, + samples: [], + } + + // 提取所有 samples + mp4File.setExtractionOptions(videoTrack.id ?? 1, null, { + nbSamples: videoTrack.nb_samples ?? 0, + }) + mp4File.start() + } + + mp4File.onSamples = (_trackId: number, _user: unknown, samples: Sample[]) => { + if (!meta) { + resolve(null) + return + } + + // 前端切片:按 [startTime, endTime] 时间范围过滤样本 + const timescale = meta.timescale + const startCts = segment.startTime * timescale + const endCts = segment.endTime * timescale + + // 过滤出时间范围内的样本 + let filtered = samples.filter((s) => (s?.cts ?? 0) >= startCts && (s?.cts ?? 0) < endCts) + + // 确保从关键帧开始(跳过第一个 sync 之前的非关键帧) + let foundSync = false + filtered = filtered.filter((s) => { if (s.is_sync) { - sync = true + foundSync = true return true } - return sync + 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)`, + ) + + // ✅ 关键修复:等待 onSamples 完成后再返回 + resolve(meta) } - 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}`) + resolve(null) + } - mp4File.onError = (_module: string, message: string) => { - console.error(`[useCanvasPlayer] MP4Box 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) - - return meta + // 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) + }) }, [segments, extractCodecDescription], ) @@ -666,12 +676,12 @@ export function useCanvasPlayer( if (isDestroyedRef.current) return - // 2. 解析每个片段的轨道元数据 + // 2. 解析每个片段的轨道元数据(await 等待 onSamples 回调完成) const metas: SegmentMeta[] = [] for (let i = 0; i < segments.length; i++) { const buffer = segmentDataRef.current.get(segments[i].assetId) if (!buffer) continue - const meta = demuxSegment(buffer, i) + const meta = await demuxSegment(buffer, i) if (meta) metas.push(meta) }