You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

使用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>

关键修改点说明

  1. 提取SPS/PPS:通过拆分关键帧中的NAL单元,筛选出类型为7(SPS)和8(PPS)的NAL单元
  2. 构造AVCDecoderRecord:按照H.264的AVC解码器记录格式,将SPS和PPS封装成description字段需要的二进制数据
  3. 更新解码器配置:将description添加到VideoDecoderConfig中,确保解码器初始化时获取到必要的视频参数

额外注意事项

  • 确保你的codec字符串(avc1.42E01E)和SPS中的profile参数匹配,否则可能出现兼容性问题
  • 如果提取SPS/PPS失败,可以检查关键帧的base64数据是否完整,是否确实包含了SPS和PPS单元

备注:内容来源于stack exchange,提问作者stackexplorer0202

火山引擎 最新活动