资讯动态

WebCodecs视频解码器配置:codec与description实战解析

发布时间:2026/10/8 9:11:26 来源:尧图企业网站定制
把 WebCodecs 的 VideoDecoder 跑通难的不是 API 本身而是配置项。我之前在公司做低延迟播放内核被问得最多的就是 codec 和 description 这两个字段codec 到底填什么description 到底是哪个二进制为什么明明按文档传了还是报错。这篇文章就把这两个字段从原理到实战彻底讲清楚包括怎么从 MP4 里把配置信息导出来、怎么手写 H.264 的 avcC、怎么排查配置成功但不出画面这类经典问题。适合用 WebCodecs 做播放器、低延迟链路、音视频工具脚本的开发者也适合那些刚入门、在官方文档面前一脸懵的朋友。1. 为什么配置 codec 和 description 让人头大先搞懂解码器的初始化协议1.1 WebCodecs 不是猜谜解码器传统video标签处理视频时浏览器内部做了大量推导工作容器解析、参数集扫描、流状态维护基本是把它丢给播放器剩下不用管。但 WebCodecs 把解码器暴露成最底层的状态机它要求你在创建解码器之前就把流的身份信息讲清楚。拿 H.264 来说一个码流能正常解码依赖 SPS序列参数集里的分辨率、帧率、profile、level以及 PPS图像参数集里的熵编码模式、参考帧数量等信息。如果这些信息缺失解码器连一帧都解不出来。传统播放器可以扫描码流自动找参数集但 WebCodecs 的 API 形状是先创建解码器、再喂数据没有留扫描的余地所以必须由你提前准备好。这就是 codec 和 description 存在的意义codec 告诉解码器这是什么编码格式、什么档次、什么等级description 把参数集或配置记录直接塞给解码器省得它自己猜。1.2 description 就是容器里那个配置 box 的数字化身MP4 里每个视频 track 都会携带一个配置 box比如 H.264 的 avcC、HEVC 的 hvcC、AAC 的 esds。这个 box 里存的就是解码器需要的说明书版本、profile、level、SPS、PPS、长度前缀格式等等。在 WebCodecs 的 config 里description字段就是这个配置记录的原始字节通常是一个ArrayBuffer或Uint8Array。解码器初始化时读一次之后每个 EncodedVideoChunk 只需要带经过长度前缀标记的样本数据不需要反复携带参数集。你可以把 codec 理解成告诉对方这是哪种文档格式把 description 理解成把文档规范细则一起递过去。只给格式名不给细则对方大概率读不了。1.3 浏览器报错信息经常不指向真正问题实际使用中VideoDecoder 的 error 回调会给一个DOMException常见的消息有Failed to parse description、NotSupportedError、TypeError。但这些报错往往只告诉你这一层挂了不会告诉你到底是 codec 字符串写错、description 里 SPS 少了还是长度前缀对不上。所以你需要自己掌握排查链路后面我会专门写一节。2. codec 字符串逐段拆解avc1/hvc1/vp09/av01 的命名规则与获取办法2.1 avc1.PPCCLLH.264 的 profile、constraint 和 levelWebCodecs 里 H.264 的标准 codec 字符串格式是avc1.PPCCLL这六个十六进制字符分三段PPprofile_idc比如 42 表示 Baseline、4D 表示 Main、64 表示 High。CCconstraint flags表示流遵守的约束条件很多编码器输出 00 或 E0。LLlevel_idc表示档位十六进制的 1F 就是 Level 3.128 是 Level 4.033 是 Level 5.1。常见写法可以参考下面这张表codec 字符串含义avc1.42001FBaseline Level 3.1低画质场景常见avc1.4D0028Main Level 4.0avc1.640028High Level 4.0很多 1080p 视频用这个avc1.640033High Level 5.1高码率 4K 片源常见注意avc1表示码流符合 AVCC 格式长度前缀分隔 NALU如果某些工具输出的是avc3那是另一种样本格式。WebCodecs 里一般用avc1。2.2 hvc1 与 hev1H.265 的复杂参数序列H.265 的 codec 字符串比 H.264 长得多格式是hvc1.加上一串用点分隔的十六进制参数具体包含 profile、compatibility 标志、约束标志、level。比如hvc1.1.6.L93.B0就是很常见的 H.265 Main Profile Level 5.1 写法。这里有个重要区别hvc1表示参数集VPS/SPS/PPS放在 stsd 的 hvcC 配置记录里hev1表示参数集可能直接带在关键帧样本里。WebCodecs 配置中理论上两者都支持但我实测下来优先用hvc1更稳因为这和把 hvcC 作为 description 传入的姿势最匹配。H.265 的 codec 字符串手写很容易出错实际项目里我建议直接靠工具生成不要自己拼。后面会讲用 ffprobe 的完整办法。2.3 vp09 和 av01WebRTC 时代越来越常见的编码VP9 的 codec 字符串类似vp09.PP.LL.DD...其中 PP 是 profileLL 是 levelDD 是 bit depth 等参数。一个能用的最小写法是vp09.00.10.08表示 profile 0、level 1.0、8 位色深。更严格的做法是从 ffprobe 拉完整参数串。AV1 的字符串格式是av01.PP.LL.TT.DD...帧常见的是av01.0.08M.080 代表 profile 008M 代表 level 8.0 主档最后的 08 代表 8-bit。这些字符串通常不需要你手写ffprobe 或 mp4box 会给。2.4 用 ffprobe 把 codec 字符串和 time_base 一次拿全我强烈建议把这个命令记下来。它不仅帮你拿到 codec 相关字段还能用 time_base 帮你换算 WebCodecs 需要的时间戳ffprobe -v quiet \ -select_streams v:0 \ -show_entries streamcodec_name,profile,level,width,height,coded_width,coded_height,time_base,avg_frame_rate,extradata,extradata_size \ -of json input.mp4输出大概长这样{ streams: [ { codec_name: h264, profile: High, level: 51, width: 1920, height: 1080, coded_width: 1920, coded_height: 1080, time_base: 1/15360, avg_frame_rate: 25/1, extradata: 015764...., extradata_size: 47 } ] }注意 ffprobe 输出的level是十进制整数比如 51 对应 Level 5.1转成十六进制就是 0x33。profile是字符串 High对应 profile_idc 0x64。如果 profile 是 Baseline 则对应 0x42Main 对应 0x4D。组合起来就是avc1.640033也就是上面表格里的 High5.1。time_base这个字段很容易被忽略但它直接决定 WebCodecs 的 EncodedVideoChunk 时间戳换算。WebCodecs 的时间戳单位是微秒如果你从 MP4 容器里拿到的 pts 是基于 time_base 的整数需要换算成微秒pts_us pts * 1000000 / (1 / time_base)比如 time_base 是 1/15360那么一帧对应的微秒数是pts * 15360 / 1000000实际公式不要记反后面我会再讲一次。音频也类似AAC 的 codec 字符串通常是mp4a.40.2这种格式AudioDecoder 的 description 需要传 AudioSpecificConfig。做法和视频一致用 ffprobe 从音频流拿 extradata。3. description 从哪来三条靠谱的提取路径3.1 路径一ffprobe 直接导出 extradataffprobe 的extradata字段就是容器里的配置记录对 H.264 来说就是 avcC 的内容。命令行这样写ffprobe -v error \ -select_streams v:0 \ -show_entries streamextradata,extradata_size \ -of defaultnoprint_wrappers1 input.mp4输出extradata01576400... extradata_size47得到的是一串十六进制字符串。后面你用 JavaScript 转成Uint8Array即可function hexToUint8Array(hex) { const bytes new Uint8Array(hex.length / 2); for (let i 0; i bytes.length; i) { bytes[i] parseInt(hex.slice(i * 2, i * 2 2), 16); } return bytes; }大多数 MP4 文件的 avcC 很小通常几十字节ffprobe 不会截断。但如果遇到 HEVC 的 hvcC 包含很多参数集或者视频是多轨的你就得用别的方法。3.2 路径二在浏览器里从 init segment 解析如果你做的不是本地上传文件播放而是 MSE/DASH 场景你手里没有物理 MP4 文件只有网络请求拿到的 init segment。这时候可以在浏览器里用 mp4box.js 或类似库解析。一个大致的做法是const mp4box MP4Box.createFile(); mp4box.onError (e) console.error(e); mp4box.appendBuffer(initSegment); const info mp4box.getInfo(); info.tracks.forEach((track) { console.log(track.codec); // 类似 avc1.640028 // 再往下挖 track 的 stsd 配置数据 });用 mp4box.js 一定要确认拿到的二进制是否带 box header。WebCodecs 的description要求的是从configurationVersion开始的配置记录内容不包含 avcC 这个 box 的 type 和 size 头。如果库返回的是完整 box你需要去掉前 8 字节再传给解码器。3.3 路径三从 Annex B 裸流里抓 SPS/PPS这个场景比较硬核适合做直播接收端。如果你的输入不是 MP4而是 RTMP、WHIP、裸 H.264 流那么码流里 SPS 和 PPS 会以 start code00 00 01或00 00 00 01开头NAL type 为 7 的是 SPS8 的是 PPS。抓取思路很简单扫描 start code。读 start code 后第一个字节的低 5 位判断 NAL type。如果是 7 或 8从 start code 后面截取到下一个 start code 为止这段就是 SPS 或 PPS。有了 SPS/PPS 之后用下一章的方法把它们打包成 avcC。这个方法对于首帧关键帧前面的参数集尤其好用因为代码逻辑不复杂后面我会给一个可以放到项目里的 helper。4. 手写 avcC 配置从 SPS/PPS 到 VideoDecoder 能吃的 ArrayBuffer4.1 avcC 的字节布局AVCDecoderConfigurationRecord 的布局是固定的直接用字节表来看偏移长度含义01configurationVersion固定为 111AVCProfileIndication即 SPS 第 1 字节21profile_compatibility即 SPS 第 2 字节31AVCLevelIndication即 SPS 第 3 字节41高 6 位保留低 2 位是 lengthSizeMinusOne51高 3 位保留低 5 位是 numOfSequenceParameterSets62第一个 SPS 的长度8NSPS 内容...1numOfPictureParameterSets...2第一个 PPS 的长度...MPPS 内容因为 H.264 的 SPS 前四个字节天然就是 profile、compatibility、level所以 avcC 前四个字节可以直接从 SPS 拷贝。lengthSizeMinusOne表示后续每个 EncodedVideoChunk 里 NALU 的长度前缀用了几个字节减一常见值是 3代表 4 字节大端长度前缀。4.2 makeAvcC 完整代码下面这个函数可以把 SPS 和 PPS 打包成 WebCodecs 能直接用的ArrayBufferfunction makeAvcC(sps, pps) { // 6 字节头部 2 字节 SPS 长度 SPS 内容 // 1 字节 PPS 数量 2 字节 PPS 长度 PPS 内容 const totalSize 6 (2 sps.length) 1 (2 pps.length); const buffer new ArrayBuffer(totalSize); const view new DataView(buffer); const bytes new Uint8Array(buffer); let off 0; view.setUint8(off, 1); // configurationVersion view.setUint8(off, sps[1]); // AVCProfileIndication view.setUint8(off, sps[2]); // profile_compatibility view.setUint8(off, sps[3]); // AVCLevelIndication view.setUint8(off, 0xff); // 11111111低 2 位为 3 view.setUint8(off, 0xe1); // 111 00001低 5 位表示 SPS 数量为 1 view.setUint16(off, sps.length, false); off 2; bytes.set(sps, off); off sps.length; view.setUint8(off, 1); // numOfPictureParameterSets 1 view.setUint16(off, pps.length, false); off 2; bytes.set(pps, off); return buffer; }调用方式const sps hexToUint8Array(67640028...); // 从流中抓到的 SPS const pps hexToUint8Array(68ebec...); // 从流中抓到的 PPS const description makeAvcC(sps, pps); const support await VideoDecoder.isConfigSupported({ codec: avc1.640028, description, codedWidth: 1920, codedHeight: 1080, });T 提示sps[1]、sps[2]、sps[3]使用的前提是你传入的 SPS 是完整的 NAL unit且第一个字节是 NAL header0x67 开头第二个字节才是 profile_idc。如果工具给出的是去掉 NAL header 的原始 SPS 数据需要自己调整偏移。4.3 lengthSizeMinusOne 和样本数据格式的对应关系这是配置成功之后最容易翻车的地方。WebCodecs 对 H.264 样本数据的要求和 MP4 的 AVC 样本格式一致每个 NALU 前面都有长度前缀长度前缀的字节数由 description 里的lengthSizeMinusOne 1决定。如果 description 里设置的是0xfflengthSizeMinusOne3那么喂给 EncodedVideoChunk 的 data 就必须是这种形态[4字节大端长度][NALU内容][4字节大端长度][NALU内容]...但如果你手上的原始码流是 Annex B 格式直接喂进去会报错或花屏。需要先把 start code 分隔的 NALU 转成长度前缀分隔。一个简单的转换逻辑function annexbToLengthPrefixed(annexb) { const naluList []; let i 0; // 扫描 start code while (i annexb.length - 4) { if (annexb[i] 0 annexb[i 1] 0) { if (annexb[i 2] 1) { naluList.push(i 3); i 3; continue; } else if (annexb[i 2] 0 annexb[i 3] 1) { naluList.push(i 4); i 4; continue; } } i; } // 根据 start code 位置切分并改写 const out []; for (let j 0; j naluList.length; j) { const start naluList[j]; const end j 1 naluList.length ? naluList[j 1] - (annexb[naluList[j 1] - 3] 1 ? 3 : 4) : annexb.length; const nalu annexb.subarray(start, end); const len nalu.length; if (len 0xffffffff) throw new Error(NALU too large); out.push(new Uint8Array([(len 24) 0xff, (len 16) 0xff, (len 8) 0xff, len 0xff])); out.push(nalu); } const total out.reduce((acc, arr) acc arr.length, 0); const result new Uint8Array(total); let offset 0; for (const arr of out) { result.set(arr, offset); offset arr.length; } return result; }这一步做完再配合正确的 description 和 codec解码器大概率就能正常出帧了。5. 配置成功后依然翻车实测中的坑与排查清单5.1 isConfigSupported 返回 false先查这几个字段VideoDecoder.isConfigSupported(config)返回supported: true才说明浏览器认可你的配置。如果返回 false优先检查codec 字符串是否按 RFC 6381 写。H264、x264、h.264这种写法都不行必须avc1.PPCCLL。大小写是否准确。比如AVC1在部分浏览器里也通过但最好统一小写。description 是否真的是配置记录不带多余的 box header。硬件不支持当前 profile。比如部分移动端对 HEVC High 10、AV1 10-bit 的硬解支持有限prefer-hardware会导致 isConfigSupported 返回 false。这里分享一个排查技巧先用support.config看看浏览器处理后到底收下了什么再和自己传的原始 config 对比。经常能发现浏览器把codedWidth/codedHeight做了修正。5.2 配置成功但解码一直报错、黑屏如果 isConfigSupported 通过了但 VideoDecoder 的 error 回调疯狂触发最常见的原因有两类第一类description 和实际码流不一致。比如你从一个视频文件里导出的 avcC却拿去解另一个分辨率、另一个 profile 的流。SPS 和 PPS 是强绑定关系任何一个字段不对都可能解不出来。项目里务必保证一个轨道对应一个 description不要缓存复用。第二类样本数据格式不对。就是上一节说的 Annex B 和长度前缀混用问题。还有一种情况是 MP4 里恰好用 2 字节长度前缀但你写死 4 字节。如果在特殊封装里记得先读 avcC 的第 4 个字节动态解析出 lengthSizeMinusOne再按对应规则处理样本。5.3 时间戳单位微秒不是毫秒EncodedVideoChunk 的timestamp和duration单位都是微秒这个我踩过很多次。如果你的 demuxer 给出的时间戳是以毫秒或秒为单位需要换算微秒 秒 * 1000000 微秒 毫秒 * 1000如果是 MP4 容器给出的 pts/dts基于 track 的 timescale那么微秒 pts / timescale * 1000000顺手把 timescale 也存下来做音视频同步时会省很多事。很多人配置全对、解码也正常但播放时画面一顿一顿十有八九是时间戳单位没换算。5.4 颜色空间和显示尺寸偏色问题往往被忽略WebCodecs 的 config 里有colorSpace字段包含 primaries、transfer、matrix、fullRange。如果视频是广色域或者 HDR但不声明 colorSpace浏览器可能按默认 BT.709 去解释结果就是偏灰、偏绿、颜色发闷。codedWidth/codedHeight和实际显示尺寸不一定一样。有些视频为了对齐编码宏块coded 尺寸会大于 visible 尺寸比如 1922x1082 这样。codedWidth/codedHeight应该填编码后的真实尺寸显示时再按宽高比缩放不要直接把 1920x1080 填进去。如果你拿到 coded 尺寸后仍出问题可以用VideoFrame.displayWidth/displayHeight做最终展示裁剪。5.5 常见问题排查决策表症状最可能原因处理方式isConfigSupported 抛 TypeErrorcodec 字符串不符合规范用 ffprobe 重新生成 codec 字符串isConfigSupported 返回 supported: falsedescription 格式不对或硬件不支持 profile检查 avcC 是否从 configurationVersion 开始尝试 no-preferenceerror 回调报 Failed to parse descriptiondescription 不是合法的配置记录确认没有把 box header 传进去解码成功但黑屏description 与当前流不匹配或样本格式不对更新 description检查 NALU 长度前缀画面一顿一顿时间戳单位错误统一换算为微秒颜色偏绿发灰缺少 colorSpace 配置从 ffprobe 拉 color_primaries/color_transfer/color_space 并映射进 config5.6 实际项目中什么时候手写什么时候交给库如果你只是做播放器外壳建议用 mediabunny、hls.js 的 WebCodecs 分支这类现成库它们已经帮你处理好了 demux、description 提取、chunk 时间戳换算不需要自己写。但如果你想做低延迟推流预览、自定义解码器、或者想真正理解 WebCodecs那字节级的 parsing 能力是绕不开的。我自己现在排查问题时的习惯是拿到一个打不开的视频第一件事就是 ffprobe 把 extradata 拉出来用 hex 编辑器对一遍 SPS/PPS再决定是不是 description 的锅。大多数疑难杂症到这一步就已经定位了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑