做 WebCodecs 的同学,十有八九都在configure()这一步摔过跤。明明VideoDecoder、AudioDecoder的 API 一看就懂,真到了要给浏览器传codec和description的时候,要么编解码器初始化直接抛错,要么画面出来全是花屏马赛克。原因往往就一个:codec字符串格式不对,或者description缺了关键的 extradata 数据。这篇文章就专门把这两个配置项掰开揉碎讲清楚,从编码器命名规则到 MP4 盒子解析,再到常见的报错排查,让你一次就能配置到能跑的程度。
codec和description这俩参数,本质上就是在告诉浏览器“你要用哪种编码格式来处理数据,以及处理前需要哪些初始化字节”。搞清楚它们,不只是能解决眼前报错,往后你自己封装一个 WebCodecs 播放器、做视频裁剪、做实时流处理,都能少踩一把坑。
1. WebCodecs 中 codec 与 description 到底管什么
1.1 codec 字符串不是随便填的
WebCodecs 里的codec不是普通的文件后缀名,它是一个遵循 RFC 6381 的编码标识字符串。比如常见的"avc1.640029"、"mp4a.40.2"、"vp09.00.10.08"。这个字符串里嵌入了编码标准、profile、level、constraint 等信息,浏览器会根据它来找到对应的解码器实例,并决定怎么初始化底层的软件或硬件解码模块。
很多人第一次会把它和 MIME type 搞混。"video/mp4; codecs=avc1.640029"中avc1.640029才是我们需要的 codec 字符串,video/mp4是容器类型。WebCodecs 的VideoDecoderConfig.codec只需要那一段avc1.640029,不要带video/mp4; codecs=前缀。就这个前缀问题,我见过不少新手直接把整串 MIME 塞进去,结果浏览器一直在报TypeError: Failed to execute 'configure' on 'VideoDecoder'。
codec 字符串的写法直接影响到浏览器选择硬件加速还是软件解码。比如 H.264 的avc1和avc3,前者表示解码时使用 AVCDecoderConfigurationRecord 里的 lengthSize 来分割 NAL unit,后者表示使用 Annex B 格式的 Start Code 来分割。如果你传的是一段已经从 MP4 里解封装出来的avcC格式数据,但 codec 字符串却写了avc3,那 Chrome 很可能会认为数据格式不匹配,导致解码出来的画面错乱。
1.2 description 为什么这么关键
description 是 WebCodecs 里的一个BufferSource类型的字段,官方文档管它叫“编码器或解码器所需的初始化数据”。这个东西听起来抽象,其实就是我们从媒体文件里提取出来的 extradata,例如 H.264 的 AVCDecoderConfigurationRecord、H.265 的 HEVCDecoderConfigurationRecord、AAC 的 AudioSpecificConfig。
这些数据包含了编码时使用的 profile、level、SPS/PPS 参数集、AAC 采样率索引、通道配置等核心信息。好比你要让别人解密一封压缩信,他光知道“这是 zip 格式”还不够,还得先拿到那串解压密码。description 就是解码器需要的“密码”。
没有 description 时,解码器对于某些格式也能启动,比如 H.264 使用 Annex B 封装且每个关键帧前都带 SPS/PPS 时,可以不要 description。但一旦遇到从 MP4、MKV 容器中解封装出来的数据,大概率是 length-prefixed 的类型,SPS/PPS 不在媒体数据流里,而是集中放在容器的 metadata 中,这时候没有 description,解码必然出错或者直接无法 configure。
1.3 什么时候可以省略 description
也不是所有场景都必须给 description。对于vp8、vp09、av01这类编码格式,WebCodecs 通常可以完全从 codec 字符串和原始帧数据中获取足够信息,因此VideoDecoderConfig.description可以留空。opus格式的音频也一样,codec 字符串写"opus"就够,不填 description 也能正常工作。
但对于 H.264、H.265、AAC 这几类老牌编码,description 基本都是必修课。尤其是 H.264,如果你只是播放器里从 MP4 中拿数据,description 部分必须得从容器里把avcCBox 抠出来。一句话总结:codec 告诉浏览器“格式是什么”,description 告诉浏览器“这个码流具体怎么解析”。两者配合好了,后面才谈得上解码效率和兼容性。
2. 常用 codec 字符串的规范写法
2.1 H.264/AVC 的 avc1 与 avc3 差异
H.264 的 codec 字符串标准形态是avc1.[profile][constraint][level],其中 profile、constraint、level 分别用两位十六进制表示,连起来是六位十六进制。比如avc1.42001f表示 Baseline profile、Level 3.1;avc1.64001f表示 High profile、Level 3.1;avc1.640029表示 High profile、Level 4.1。
这后面几位的取值不是你在文件属性里看到的那种十进制数,而是要转成十六进制。我经常碰到朋友拿avc1.100.31这种写法来问我为什么不行,因为他以为 profile 直接写 100(十进制)、level 写 31(十进制)就够了。实际上浏览器认的是十六进制的整数。100(十进制)= 0x64,31(十进制)= 0x1F,所以合法的字符串应该是avc1.64001F,大小写都行,但位数要对齐。level 值 41 对应十六进制 0x29,有些地方写成avc1.640029就是这么来的。
avc3是 H.264 的另一种封装标识。在 MP4 里的avc3sample entry 表示 NAL unit 长度字段不一定和 avcC 里的 lengthSizeMinusOne 一致,而是使用 4 字节长度前缀。从 MKV 或者 WebM 转到 WebCodecs 时,偶尔会遇到这种格式。如果你不确定手里的数据是avc1还是avc3,最稳妥的办法是直接看容器的 sample entry type,别猜。
2.2 H.265、AAC、Opus、VP8/VP9、AV1 的写法
H.265 的标准写法是hvc1.[profile_space][profile_idc][profile_compat][level_idc][constraint_indicator],不同浏览器对 H.265 的支持差异较大,Chrome 桌面版在支持硬件解码时通常识别hev1或hvc1,然后跟上1.6.L93.B0之类的字符串。相比 H.264,H.265 的 codec 字符串更长也更复杂,实际使用时建议直接从源文件的 track header 或者 ffprobe 输出里复制。
AAC 音频的 codec 常见写法是mp4a.40.2,其中40表示 MPEG-4 Audio,最后的2表示 Audio Object Type 为 AAC LC。AAC HE-AAC 会显示mp4a.40.5或mp4a.40.29,这取决于 SBR 和 PS 的配置。有时候你遇到mp4a.40.2无法解码,可能实际音频是 HE-AAC,只有把 object type 改成 5 才能识别。
Opus 相对简单粗暴,直接填"opus",WebCodecs 不认识其他的花哨写法。VP8 填"vp8",VP9 的格式是vp09.00.10.08,分别代表 profile、level、bit depth、chroma subsampling。AV1 更啰嗦,av01.0.04M.08这种,其中 0 是 profile,04 是 level,M 代表 8 比特主档。这些细节无需死记,遇到就查,查完就写进自己的笔记里。
2.3 用 ffmpeg 查看 codec 配置
在开始手写 codec 字符串之前,建议先用 ffprobe 看看源文件到底是怎么编码的。一行命令就能拿到:
ffprobe -v trace -show_entries stream=codec_name,profile,level,codec_tag_str,extradata_size -of json input.mp4输出里面会有codec_name: h264、profile: High、level: 40、codec_tag_string: avc1。level 显示 40 表示四级,转成十六进制就是 0x28,但 profile 和 level 之间还需要一个 constraint 字节,通常填 00,于是完整的 codec 字符串可能是avc1.640028。
如果源文件是 AAC,ffprobe 会直接显示codec_name: aac,然后在extradata字段里有类似4010的十六进制串,这正好对应 AudioSpecificConfig。用这段 hex 转成 Uint8Array 就可以作为 description 使用。ffprobe 的-v trace参数很关键,它能把容器底层的 header 信息打出来,尤其是extradata_size为 0 时,说明这个文件可能没有额外初始化数据,那么 description 省略也是可以接受的。
3. description 的提取与构造
3.1 从 MP4 中挖出 avcC/hvcC/esds
MP4 文件的内部结构可以理解为一层套一层的盒子。视频解码信息存放在moov/trak/mdia/minf/stbl/stsd/avc1/avcC这条路径上。用十六进制编辑器打开文件,找avcC四个字母,然后把从avcC之后开始的整个 Box 内容截出来即可。
尽管听起来简单,真正手撕 MP4 时容易出错。avcCBox 通常在文件靠后位置的moov里,而且它是大端序存储,长度字段占 4 字节。我建议不要靠肉眼去 binwalk,直接写脚本解析更靠谱。解析的时候也可以顺带把 SPS/PPS 的长度、SPS 数据、PPS 数据都读出来,这些字段在后续有特殊需求时都会用到。
对于 AAC,对应的盒子是esds。ESDS 内部结构较复杂,里面嵌入了DecSpecificInfo,也就是 AudioSpecificConfig。手工提取时不用解析到最深层,直接把esds中0x05标签后面的数据截出来就基本是我们要的内容。这也是很多 JS 库只处理esds的前面几个字节,然后整体塞给 WebCodecs 的原因。
3.2 代码级解析:从 MP4 到 description
下面给出一个在实际项目中验证过的简化版解析函数,能用最低成本从 MP4 文件里抽出 H.264 的 description:
function findBox(data, type) { const typeStr = type.replace(/(.{4})/g, '$1'); let offset = 0; while (offset + 8 <= data.length) { const size = (data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]; const boxType = String.fromCharCode(data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7]); if (boxType === type) { return data.subarray(offset, offset + size); } offset += size; } return null; } function walkToMoov(data) { let moov = findBox(data, 'moov'); // 递归找 trak/stsd/avcC 的完整实现这里省略 // 核心思路:每个 box 内继续按 offset 扫描 }实际项目中,建议直接使用mp4box.js或mediabunny,它们已经把 MP4 解析封装得比较完善。只需要拿到解析结果后把它提供的avcC二进制数据塞给 description。如果你只用mp4box.js里的getTrackById()返回对象,里面有个avcC字段,类型为 Uint8Array,直接传给VideoDecoderConfig.description即可。音频就用esds里的AudioSpecificConfig,在 mp4box.js 中通常表现为esds字段。
从 WebM 里拿 description 的场景也经常出现,但 WebM 的初始化数据位置不叫avcC,而叫CodecPrivate。Element 里存储的是 Xiph 或 A_OPUS 的头信息,对于 H.264 在 WebM 里基本没有,直接省略即可。VP8/VP9 在 WebM 里也不需要 description,所以你的配置逻辑应该区分容器类型:MP4 走avcC/hvcC/esds,WebM 走CodecPrivate,裸流跳过。
3.3 annexb 与 avcC 的转换
WebCodecs 解码 H.264 时,视频帧数据有两种常见的组织形态:Annex B 和 AVCC。Annex B 使用00 00 00 01或00 00 01作为 NAL unit 分隔符,AVCC 则在每个 NAL unit 前用长度字段表示该单元的长度,长度字段的字节数由 description 中lengthSizeMinusOne指定,通常是 4 字节。
如果你的输入数据是 Annex B 格式,description缺失时也可以解码,只要你把所有 SPS/PPS 放在每个 IDR 帧前面。但如果你从 MP4 里解封装出来的数据是 AVCC 格式,那就必须把avcC给 description,否则解码器找不到 SPS/PPS。反过来,如果你只有裸流的 Annex B 数据,又想要 MP4 里那样的 avcC description,可以用 ffmpeg 转一下:
ffmpeg -i input.h264 -c copy -bsf:v h264_mp4toannexb output.mp4这条命令实际是把 AVCC 格式转成 Annex B 格式。反向操作也有对应的 bitstream filter:h264_annexbto_mp4。理解了两种格式的区别之后,你会发现很多 WebCodecs 报错都源于格式错配,比如 codec 写了avc1,数据却是 Annex B,或者反过来。
4. 配置示例:用 WebCodecs 解码 H.264 与 AAC
4.1 解码 H.264 视频轨道
配置一个 H.264 视频解码器,需要组装VideoDecoderConfig:
const config = { codec: 'avc1.640029', codedWidth: 1920, codedHeight: 1080, description: avcCBuffer, // 从 mp4box.js 获取的 Uint8Array optimizeForLatency: false }; const decoder = new VideoDecoder({ output: (frame) => { // 处理解码后的 VideoFrame frame.close(); }, error: (e) => { console.error('Decode error:', e); } }); decoder.configure(config); const chunk = new EncodedVideoChunk({ type: 'key', timestamp: 0, duration: 40000, data: encodedVideoData }); decoder.decode(chunk);这里encodedVideoData是从 MP4 解封装得到的一个 sample 数据。用 mp4box.js 的onSamples回调可以直接获取sample.data,把sample.is_sync映射为type: 'key'或'delta',sample.cts映射为timestamp。需要特别注意:WebCodecs 的 timestamp 单位是微秒,而 mp4box.js 默认使用 90000 时间基的 CTS,需要换算成微秒,也就是乘以1000000 / timescale。
如果你用的是 fMP4 在线拉流,每段samples里都会带description相关的 Box 信息,直接在 seek 时重新 configure 即可。但要是你直接把整段 MP4 塞给浏览器的demuxer,第一次 configure 拿到了 description,后面分别离轨的 sample 里就不需要再重复配置了,只需要持续 decode 即可。
4.2 解码 AAC 音频轨道
音频解码器的配置与视频一样,需要提供 codec 与 description。对于 AAC,codec 一般是mp4a.40.2,description 是 AudioSpecificConfig:
const audioConfig = { codec: 'mp4a.40.2', sampleRate: 44100, numberOfChannels: 2, description: audioSpecificConfigBuffer }; const audioDecoder = new AudioDecoder({ output: (frame) => { // frame 是 AudioFrame,可以转成 Float32Array 或写进 AudioWorklet frame.close(); }, error: (e) => { console.error('Audio decode error:', e); } }); audioDecoder.configure(audioConfig);AAC 的 AudioSpecificConfig 只有两个字节比较多见,例如0x12 0x10表示 AAC LC、44.1kHz、双声道。如果你有多个 AudioSpecificConfig 变体,需要保留与当前音频轨真正匹配的那一份。错误匹配采样率会让声音变调或完全无声,这是 WebCodecs 音频开发里最隐蔽的问题。
如果浏览器提示codec string not supported,先别急着怀疑浏览器,先看自己的 codec 字符串是否来自 ffprobe 的输出。很多编码器会把 AAC object type 定为 2,也就是mp4a.40.2,但个别文件里会写成 5(HE-AAC),这就是mp4a.40.5。你要是写错一位,Chrome 可能直接给你NotSupportedError。
4.3 编码端如何处理 description
不仅是解码器需要配置,使用VideoEncoder和AudioEncoder时同样会遇到 description。编码器会在output回调里返回一个新的EncodedVideoChunk,紧接着回调里的metadata参数中带有decoderConfig,这个decoderConfig.description就是需要保存下来的编码描述。
const encoder = new VideoEncoder({ output: (chunk, metadata) => { if (metadata && metadata.decoderConfig) { // 保存 decoderConfig.description,后续解码时使用 saveDescription(metadata.decoderConfig.description); } saveChunk(chunk); }, error: (e) => console.error(e) }); encoder.configure({ codec: 'avc1.640029', width: 1280, height: 720, bitrate: 2_000_000, framerate: 30 });这段逻辑很重要。很多自己做 MPC 格式封装的工具,第一步就是把编码器输出的decoderConfig.description拼进文件头,播放时再提取出来。如果不做这一步,生成的音视频文件放到别的播放器里就会黑屏或无声。留意metadata.decoderConfig为空的情况,这通常出现在首帧输出时,因为 H.264 编码器还没生成关键帧。多攒几个 chunk 后再保存 description 比较保险。
5. 常见报错与排查技巧
5.1 典型错误信息对照表
| 报错信息 | 常见原因 | 解决思路 |
|---|---|---|
TypeError: Failed to construct 'VideoDecoder' | 浏览器不支持 WebCodecs | 检查 Chrome 版本,需要 94+ |
NotSupportedError | codec 字符串不合法或浏览器不支持该编码 | 用 ffprobe 核对 profile/level 再转十六进制 |
DataError: A parser error occurred | description 缺失或解码数据格式不匹配 | 补充 avcC 或转换为 Annex B 格式 |
InvalidStateError | configure 之后没有正常 reset 或 state 错误 | 检查decoder.state是否为configured |
| 解码后画面花屏 | SPS/PPS 错误或 description 长度不对 | 重新提取 avcC,核对 lengthSizeMinusOne |
| 音频无声或变调 | AudioSpecificConfig 与采样率不匹配 | 读取真实音频轨的 sampleRate |
实际开发中,“花屏”比直接报错更难查。我遇到过从某个视频网站下载的 fMP4 片段,它的avcC里 SPS 正常,但 PPS 在后续一个 moof 中又出现了新的变种。这种情况下,一昧用最初那次提取的 description 就不行了,需要在播放到相应片段时重新获取并更新 decoder config。
如果是UnicodeDecodeError类的乱码报错,通常是某种工具试图用文本方式读取二进制内容,比如你在 Node.js 里用toString()方式打印了二进制 extradata,或者用错误的编码读取了文件。这个更多是调试方式的问题,不是 WebCodecs 本身的错误。记得在 Node 里用Buffer、在浏览器里用ArrayBuffer处理二进制,别用 UTF-8 字符串硬解。
5.2 时间基与 duration 的坑
时间基虽然在配置 codec 时不需要直接设置,但 WebCodecs 的EncodedVideoChunk和输出VideoFrame的timestamp与容器中的 time base 有换算关系。MP4 里的 timescale 常见是 90000 或 1000,WebCodecs 统一使用微秒。如果你直接把 MP4 的 sample 时间戳塞进 WebCodecs,会出现音画不同步。
正确的换算公式是:timestampInMicroseconds = sampleCts * 1000000 / timescale。例如 timescale 等于 15360,sampleCts 为 7680,换算后就是7680 * 1000000 / 15360 = 500000微秒,也就是 500 毫秒的位置。编码器输出的VideoFrame.timestamp也是微秒,写回 MP4 时需要反过来乘以 timescale 再除以 1000000。
5.3 我的调试流程
说一个我自己的固定调试流程:先用ffprobe -v trace把文件的 extradata 输出成 hex 字符串,拷贝到代码里转成一个Uint8Array,然后配置解码器,用第一个关键帧测试。如果这一步正常,再接入完整的 MP4 解封装流程。
如果解码报错,我会先用中文描述一下当前的问题,然后写好一个抓取decoderConfig与input chunk的 log 函数,把这段输入数据和配置对象记录下来。大多数情况下,通过对比正常文件和异常文件的avcChex 数据,很快能找出差异。很多时候问题就出现在avcC多了几个字节或者少了几个字节,而不是编解码算法本身。
另一个实用的技巧是使用 Chrome 的chrome://media-internals页面,它会记录所有 WebCodecs 相关的媒体解码器创建情况,包括是否启用了硬件加速、是否缺少 codec 参数等。这个页面比 DevTools 里的 Console 更有信息量,推荐所有做 WebCodecs 开发的朋友收藏。
最后再分享一个小经验:当你不确定某个 codec 字符串格式时,直接在浏览器里跑一遍是最快的验证方式。不要怕写错,WebCodecs 的报错信息虽然有时不够友好,但多试对比几组配置,你很快就能建立起自己对 codec 参数的感觉。编码这种活儿,最终还得靠动手积累手感。