使用WebCodecs解码HTTP获取的H.264数据并在浏览器显示时出现DataError
WebCodecs解码HTTP获取的H.264数据并在浏览器显示时出现DataError
看起来你遇到了WebCodecs解码AVC格式H.264时的常见配置问题,错误提示已经点明了核心原因:对于AVC编码的H.264,必须在VideoDecoderConfig中填充description字段——哪怕你已经传入了关键帧,解码器也不会自动从关键帧中提取SPS(序列参数集)和PPS(图像参数集)来完成初始化。
你的错误信息回顾
(index):60 Error during fetch or decode process: DataError: Failed to execute 'decode' on 'VideoDecoder': A key frame is required after configure() or flush(). If you're using AVC formatted H.264 you must fill out the description field in the VideoDecoderConfig.
at fetchAndDecodeJson ((index):55:24)
问题根源
WebCodecs的AVC解码器需要提前获取SPS和PPS来确定视频的分辨率、帧率等核心参数,这些参数被封装在关键帧的NAL单元中,但解码器不会自动解析第一个关键帧中的这些信息,必须通过description字段显式传入。
修复方案
我们需要从你的关键帧数据中提取SPS和PPS,构造符合AVCDecoderRecord格式的description,然后传入解码器配置。以下是修改后的完整代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>decode H.264 to Canvas</title> </head> <body> <canvas id="output" style="border: 1px solid black;"></canvas> <script> // 从关键帧数据中提取SPS和PPS function extractSpsPpsFromKeyFrame(rawKeyData) { let nalStart = 0; const nals = []; // 按H.264的NAL分隔符(0x00000001)拆分NAL单元 while (nalStart < rawKeyData.length) { let sepIndex = -1; // 寻找分隔符位置 for (let i = nalStart; i < rawKeyData.length - 3; i++) { if (rawKeyData[i] === 0 && rawKeyData[i+1] === 0 && rawKeyData[i+2] === 0 && rawKeyData[i+3] === 1) { sepIndex = i; break; } } if (sepIndex === -1) { // 处理最后一个NAL单元 nals.push(rawKeyData.slice(nalStart)); break; } if (sepIndex > nalStart) { nals.push(rawKeyData.slice(nalStart, sepIndex)); } nalStart = sepIndex + 4; } // 筛选出SPS(NAL类型7)和PPS(NAL类型8) const sps = nals.find(nal => (nal[0] & 0x1F) === 7); const pps = nals.find(nal => (nal[0] & 0x1F) === 8); return { sps, pps }; } // 构造AVCDecoderRecord格式的description function createAVCDecoderRecord(sps, pps) { if (!sps || !pps) throw new Error("Missing SPS or PPS"); // AVCDecoderRecord的结构定义 const recordLength = 8 + sps.length + 2 + pps.length; const record = new Uint8Array(recordLength); let offset = 0; // 版本号(固定为1) record[offset++] = 1; // 从SPS中取profile_idc record[offset++] = sps[1]; // 从SPS中取profile_compatibility_flag record[offset++] = sps[2]; // 从SPS中取level_idc record[offset++] = sps[3]; // 保留位(6位) + NAL长度字段大小-1(2位,这里用4字节长度,所以是3) record[offset++] = 0xFC | 3; // SPS数量(固定为1) record[offset++] = 1; // SPS长度(2字节) record[offset++] = (sps.length >> 8) & 0xFF; record[offset++] = sps.length & 0xFF; // 写入SPS数据 record.set(sps, offset); offset += sps.length; // PPS数量(固定为1) record[offset++] = 1; // PPS长度(2字节) record[offset++] = (pps.length >> 8) & 0xFF; record[offset++] = pps.length & 0xFF; // 写入PPS数据 record.set(pps, offset); return record; } async function fetchAndDecodeJson() { try { const response = await fetch("http://127.0.0.1:8000/datatest.json"); const jsonData = await response.json(); console.log("Fetched JSON Data:", jsonData); const canvas = document.getElementById("output"); const ctx = canvas.getContext("2d"); // 处理关键帧,提取SPS和PPS const keyFrameRaw = Uint8Array.from(atob(jsonData[0].data), c => c.charCodeAt(0)); const { sps, pps } = extractSpsPpsFromKeyFrame(keyFrameRaw); const avcDescription = createAVCDecoderRecord(sps, pps); const videoDecoder = new VideoDecoder({ output: (frame) => { canvas.width = frame.displayWidth; canvas.height = frame.displayHeight; ctx.drawImage(frame, 0, 0); frame.close(); }, error: (e) => console.error("decode error:", e), }); const codecSupport = await VideoDecoder.isConfigSupported({ codec: "avc1.42E01E", description: avcDescription }); if (!codecSupport.supported) { throw new Error("codec not supported"); } // 传入包含description的配置 videoDecoder.configure({ codec: "avc1.42E01E", description: avcDescription }); // 解码每个视频块 for (const item of jsonData) { const { type, pts, data } = item; const rawData = Uint8Array.from(atob(data), (c) => c.charCodeAt(0)); const chunk = new EncodedVideoChunk({ timestamp: pts * 1000, type: type, data: rawData, }); videoDecoder.decode(chunk); } console.log("decode success"); } catch (error) { console.error("decode or fetch error:", error); } } fetchAndDecodeJson().catch(console.error); </script> </body> </html>
关键修改点说明
- 提取SPS/PPS:通过拆分关键帧中的NAL单元,筛选出类型为7(SPS)和8(PPS)的NAL单元
- 构造AVCDecoderRecord:按照H.264的AVC解码器记录格式,将SPS和PPS封装成
description字段需要的二进制数据 - 更新解码器配置:将
description添加到VideoDecoderConfig中,确保解码器初始化时获取到必要的视频参数
额外注意事项
- 确保你的
codec字符串(avc1.42E01E)和SPS中的profile参数匹配,否则可能出现兼容性问题 - 如果提取SPS/PPS失败,可以检查关键帧的base64数据是否完整,是否确实包含了SPS和PPS单元
备注:内容来源于stack exchange,提问作者stackexplorer0202




