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
相关产品推荐
相关产品推荐

