From ae1b9f8ddb1cff40f51b083f341236bebe9dd0ee 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 22:18:26 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20findCodecConfig=20=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E7=BA=AF=E9=80=92=E5=BD=92=E6=90=9C=E7=B4=A2=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20HEVC=20description=20=E6=8F=90=E5=8F=96=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:旧 findCodecConfig 对 container box 的 fullbox 检测逻辑错误, 导致递归搜索时跳过了子 box 的起始位置,永远找不到 hvcC/avcC。 修复: - 替换为 findCodecConfigRecursive,纯递归搜索所有嵌套 box - 不区分 fullbox/非 fullbox,统一从 offset+8 开始搜索子 box - 返回 hvcC/avcC 的内容(不含 8 字节 box header),符合 WebCodecs 要求 - 添加日志追踪找到的 box 类型和位置 --- .../pages/generate/hooks/useCanvasPlayer.ts | 72 ++++++------------- 1 file changed, 23 insertions(+), 49 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts b/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts index cf775ed83..c08ce1aa4 100644 --- a/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts +++ b/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts @@ -10,14 +10,25 @@ import type { Movie, Sample } from "mp4box" // ── MP4 Box 解析辅助函数 ── -/** 在指定范围内查找 avcC / hvcC box,返回其数据 */ -function findCodecConfig(buffer: ArrayBuffer, start: number, end: number): ArrayBuffer | undefined { +/** + * 递归搜索 box 树,找到 hvcC 或 avcC box 并返回其配置数据(不含 8 字节 box header) + * + * MP4 box 嵌套结构:moov → trak → mdia → minf → stbl → stsd → hev1 → hvcC + * 此函数不区分 fullbox / 普通 box,统一从 offset+8 开始递归搜索子 box + * 因为所有 container box(moov/trak/mdia/minf/stbl)的 content 都从 offset+8 开始 + */ +function findCodecConfigRecursive( + buffer: ArrayBuffer, + start: number, + end: number, +): ArrayBuffer | undefined { const view = new DataView(buffer) let offset = start while (offset < end - 8) { const size = view.getUint32(offset) - if (size < 8) break + if (size < 8 || offset + size > end) break + const type = String.fromCharCode( view.getUint8(offset + 4), view.getUint8(offset + 5), @@ -25,53 +36,16 @@ function findCodecConfig(buffer: ArrayBuffer, start: number, end: number): Array view.getUint8(offset + 7), ) - const containerBoxes = ["moov", "trak", "mdia", "minf", "stbl"] - if (containerBoxes.includes(type)) { - // 动态检测 fullbox:检查 offset+8 处的 version/flags 是否为 0 - // 如果不是 fullbox,content 从 offset+8 开始;如果是 fullbox,content 从 offset+12 开始 - const v1 = view.getUint8(offset + 8) - const v2 = view.getUint8(offset + 9) - const v3 = view.getUint8(offset + 10) - const isFullBox = v1 === 0 && v2 === 0 && v3 === 0 - const contentStart = isFullBox ? offset + 12 : offset + 8 - const result = findCodecConfig(buffer, contentStart, offset + size) - if (result) return result - } else if (type === "stsd") { - // SampleDescriptionBox 是 fullbox: 8 header + 4 version/flags + 4 entry_count - const entryCount = view.getUint32(offset + 12) - let entryOffset = offset + 16 - console.log("[findCodecConfig] stsd found:", { - entryCount, - stsdOffset: offset, - stsdSize: size, - }) - for (let i = 0; i < entryCount && entryOffset < offset + size; i++) { - const entrySize = view.getUint32(entryOffset) - const entryType = String.fromCharCode( - view.getUint8(entryOffset + 4), - view.getUint8(entryOffset + 5), - view.getUint8(entryOffset + 6), - view.getUint8(entryOffset + 7), - ) - console.log("[findCodecConfig] entry:", { index: i, entryOffset, entrySize, entryType }) - // 视觉样本条目: 8 header + 6 reserved + 2 data_ref_index + remaining - // 子 box 从 entryOffset + 16 + 62 开始 (skip reserved + data_ref_index + predefined) - // 实际结构: 8(header) + 6(reserved) + 2(data_ref_index) + 16(predefined+reserved) + 2(width) + 2(height) + ... - // 子 box 从 entryOffset + 8 + 6 + 2 + 16 + 2 + 2 + 2 + 2 + 4 + 2 + 2 + 2 + 2 = entryOffset + 78 - // 更简单的做法:扫描 entry 内的子 box - const entryEnd = entryOffset + entrySize - const subBoxStart = entryOffset + 8 + 78 // VisualSampleEntry 固定字段共 78 字节(ISO 14496-12) - const result = findCodecConfig(buffer, subBoxStart, entryEnd) - if (result) return result - entryOffset += entrySize - } - } else if (type === "avcC" || type === "hvcC") { - // 找到目标 box,返回完整 box(含 header) - // 返回完整 box(含 size + type header),WebCodecs HEVC decoder 需要 - return buffer.slice(offset, offset + size) + // 找到目标 codec 配置 box,返回内容(不含 8 字节 header) + if (type === "avcC" || type === "hvcC") { + console.log("[findCodecConfig] Found", type, "at offset", offset, "size", size) + return buffer.slice(offset + 8, offset + size) } - if (size === 0) break + // 递归搜索子 box(所有 box 的子 box 都从 offset+8 开始) + const childResult = findCodecConfigRecursive(buffer, offset + 8, offset + size) + if (childResult) return childResult + offset += size } return undefined @@ -261,7 +235,7 @@ export function useCanvasPlayer( view.getUint8(offset + 7), ) if (type === "moov") { - const result = findCodecConfig(buffer, offset + 8, offset + size) + const result = findCodecConfigRecursive(buffer, offset + 8, offset + size) console.log("[useCanvasPlayer] extractCodecDescription:", { moovOffset: offset, moovSize: size,