You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Chrome中用WebCodecs解码Annex B格式HEVC视频报错求助

解码Annex B格式HEVC视频时遇EncodingError: Decoder error问题排查与修复

问题描述

在Chrome浏览器中使用WebCodecs API解码Annex B格式原始HEVC视频时,始终触发EncodingError: Decoder error,且无任何帧输出。操作流程为:

  • 按NAL单元拆分视频流,解析出VPS、SPS、PPS,通过WASM解析描述信息,此步骤无明显异常;
  • 将VPS、SPS、PPS与首个关键帧合并为一个chunk发送给解码器,再逐个解码delta帧,但始终无帧输出。

代码问题分析及修复方案

1. NAL单元拆分逻辑缺陷

原代码的起始码检测存在边界处理错误,会导致NAL单元拆分不完整或遗漏。修复后的拆分逻辑需更严谨:

// 修复后的NAL单元拆分函数
function splitNalUnits(data: Uint8Array): Uint8Array[] {
    const nalUnits: Uint8Array[] = [];
    let start = 0;
    const len = data.length;

    for (let i = 3; i < len; i++) {
        // 检测长起始码 0x00000001
        if (data[i] === 0x01 && data[i-1] === 0x00 && data[i-2] === 0x00 && data[i-3] === 0x00) {
            if (start !== i-3) {
                nalUnits.push(data.slice(start, i-3));
                start = i-3;
            }
            i += 3; // 跳过起始码剩余部分
        } 
        // 检测短起始码 0x000001
        else if (data[i] === 0x01 && data[i-1] === 0x00 && data[i-2] === 0x00) {
            if (start !== i-2) {
                nalUnits.push(data.slice(start, i-2));
                start = i-2;
            }
            i += 2; // 跳过起始码剩余部分
        }
    }

    // 添加最后一个NAL单元并过滤空单元
    if (start < len) nalUnits.push(data.slice(start));
    return nalUnits.filter(unit => unit.length > 0);
}

2. 解码器配置时机错误

原代码在循环中异步处理解码器配置,可能导致未完成配置就开始解码帧。需先收集所有参数集,完成配置后再处理帧:

  • 先遍历所有NAL单元,收集VPS、SPS、PPS、关键帧和delta帧;
  • 完成解码器配置后,再依次解码关键帧和delta帧,避免异步操作打乱流程。

3. EncodedVideoChunk数据格式错误

WebCodecs的EncodedVideoChunk不需要携带Annex B的起始码,需剥离后再传入:

// 剥离起始码,获取纯净NAL数据
function getNalDataWithoutStartCode(nalUnit: Uint8Array): Uint8Array {
    let startCodeLength = 3;
    if (nalUnit[0] === 0x00 && nalUnit[1] === 0x00 && nalUnit[2] === 0x00 && nalUnit[3] === 0x01) {
        startCodeLength = 4;
    }
    return nalUnit.slice(startCodeLength);
}

合并关键帧相关NAL单元时,需使用剥离起始码后的内容,而非原始带起始码的单元。

4. 时间戳与解码器状态问题

  • 原代码时间戳计算注释与实际值不符(注释30fps,实际用25fps),需统一帧率:
    const fps = 25; // 根据视频实际帧率调整
    timestamp += 1000 / fps;
    
  • 每次更新序列参数集时,需调用videoDecoder.reset()重置解码器状态,而非仅flush();
  • 解码前需检查videoDecoder.state === "configured",避免未就绪时调用decode()。

修复后的核心解码流程示例

const videoElement = document.getElementById("video") as HTMLVideoElement;
const videoDecoder = new VideoDecoder({
    output: (frame) => {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        if (ctx) {
            canvas.width = frame.displayWidth;
            canvas.height = frame.displayHeight;
            ctx.drawImage(frame, 0, 0);
            videoElement.parentNode?.insertBefore(canvas, videoElement.nextSibling);
            canvas.style.border = "1px solid black";
        }
        frame.close();
    },
    error: (err) => console.error("VideoDecoder error:", err),
});

async function decodeH265Video(file: File) {
    const arrayBuffer = await file.arrayBuffer();
    const uint8Array = new Uint8Array(arrayBuffer);
    const nalUnits = splitNalUnits(uint8Array);

    let vps: Uint8Array | null = null;
    let sps: Uint8Array | null = null;
    let pps: Uint8Array | null = null;
    let keyFrameNal: Uint8Array | null = null;
    const deltaFrames: Uint8Array[] = [];

    // 先收集所有参数集和帧
    for (const nal of nalUnits) {
        const nalType = getNalType(nal);
        switch (nalType) {
            case 32: vps = getNalDataWithoutStartCode(nal); break;
            case 33: sps = getNalDataWithoutStartCode(nal); break;
            case 34: pps = getNalDataWithoutStartCode(nal); break;
            case 19:
            case 20: keyFrameNal = getNalDataWithoutStartCode(nal); break;
            default: deltaFrames.push(getNalDataWithoutStartCode(nal)); break;
        }
    }

    // 配置解码器
    if (!vps || !sps || !pps) {
        console.error("Missing VPS/SPS/PPS");
        return;
    }
    const codecResult = await parseCodecInst.parseCodec(sps, pps, vps);
    if (!codecResult.data) {
        console.error("Failed to get codec config");
        return;
    }
    const support = await VideoDecoder.isConfigSupported(codecResult.data);
    if (!support.supported || !support.config) {
        console.error("Decoder config not supported");
        return;
    }
    videoDecoder.configure(support.config);

    // 解码关键帧
    if (keyFrameNal) {
        const mergedKeyData = mergeUint8Arrays([vps, sps, pps, keyFrameNal]);
        const keyChunk = new EncodedVideoChunk({
            type: "key",
            timestamp: 0,
            data: mergedKeyData
        });
        await videoDecoder.decode(keyChunk);
        await videoDecoder.flush();
    }

    // 解码delta帧
    let timestamp = 1000 / 25;
    for (const delta of deltaFrames) {
        const chunk = new EncodedVideoChunk({
            type: "delta",
            timestamp,
            data: delta
        });
        await videoDecoder.decode(chunk);
        timestamp += 1000 / 25;
    }
    await videoDecoder.flush();
}

// 辅助函数:获取NAL类型
function getNalType(nalUnit: Uint8Array): number {
    let startCodeLength = 3;
    if (nalUnit[0] === 0x00 && nalUnit[1] === 0x00 && nalUnit[2] === 0x00 && nalUnit[3] === 0x01) {
        startCodeLength = 4;
    }
    const nalHeader = nalUnit[startCodeLength];
    return (nalHeader & 0x7E) >> 1;
}

// 辅助函数:合并Uint8Array
function mergeUint8Arrays(arrays: Uint8Array[]): Uint8Array {
    const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
    const result = new Uint8Array(totalLength);
    let offset = 0;
    for (const arr of arrays) {
        result.set(arr, offset);
        offset += arr.length;
    }
    return result;
}

内容的提问来源于stack exchange,提问作者Cat Tom

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 15:36:03