使用AudioEncoder压缩PCM音频后Blob无法播放,求问题排查
问题:AudioEncoder生成的Blob无法播放,报错
NotSupportedError: Failed to load because no supported source was found. 我使用Web Audio API的AudioEncoder将PCM音频编码为Opus格式,代码能正常执行到flush的Promise,但生成的Blob无法被HTMLAudioElement播放,抛出上述错误。相关代码如下:
function audioBufferToF32Planar(input: AudioBuffer): Float32Array { const channels = input.numberOfChannels; const result = new Float32Array(input.length * channels); let offset = 0; for (let channel = 0; channel < channels; channel++) { const data = input.getChannelData(channel); result.set(data, offset); offset = offset + data.length; } return result; }
const outputData = new ArrayBuffer(1024, { maxByteLength: 100_000_000 }); let outputOffset = 0; const encoder = new AudioEncoder({ output: (e: EncodedAudioChunk) => { const chunkLength = e.byteLength; if (outputOffset + chunkLength > outputData.byteLength) { outputData.resize( Math.min( outputData.byteLength + chunkLength * 10, outputData.maxByteLength ) ); } const outputView = new Uint8Array(outputData); const chunkData = new Uint8Array(chunkLength); e.copyTo(chunkData); outputView.set(chunkData, outputOffset); outputOffset = outputOffset + chunkLength; }, error: console.error, });
// buffer is an AudioBuffer containing PCM audio data const data = new AudioData({ format: "f32-planar", sampleRate: buffer.sampleRate, numberOfChannels: buffer.numberOfChannels, numberOfFrames: buffer.length, timestamp: 0, data: audioBufferToF32Planar(buffer), }); encoder.configure({ codec: "opus", sampleRate: buffer.sampleRate, numberOfChannels: buffer.numberOfChannels, bitrate: 128_000, }); encoder.encode(data);
encoder.flush().then(() => { const data = outputData.transferToFixedLength(); const blob = new Blob([data], { type: "audio/webm;codecs=opus", }); // testing audio... const player = new Audio(); player.src = URL.createObjectURL(blob); player.play().then( () => { console.log("playing!"); }, (err) => { console.error(err); // <-- error here } ); player.onended = () => { URL.revokeObjectURL(player.src); }; });
问题原因与修复方案
核心问题是:AudioEncoder输出的只是原始Opus编码帧,没有封装成浏览器可识别的WebM容器格式。浏览器的音频播放器需要完整的容器结构(包含文件头、轨道信息、时间戳等元数据)才能解析播放,直接拼接裸编码帧生成的Blob不符合规范,因此报错。
具体修复步骤:
- 先收集所有编码后的
EncodedAudioChunk,而不是直接写入ArrayBuffer - 实现WebM容器封装逻辑,将Opus帧打包成符合WebM规范的字节流
- 用封装后的字节流生成Blob
修改后的完整代码:
function audioBufferToF32Planar(input: AudioBuffer): Float32Array { const channels = input.numberOfChannels; const result = new Float32Array(input.length * channels); let offset = 0; for (let channel = 0; channel < channels; channel++) { const data = input.getChannelData(channel); result.set(data, offset); offset = offset + data.length; } return result; } // 收集编码后的chunk const encodedChunks: EncodedAudioChunk[] = []; const encoder = new AudioEncoder({ output: (chunk) => encodedChunks.push(chunk), error: console.error, }); // buffer 是包含PCM数据的AudioBuffer const data = new AudioData({ format: "f32-planar", sampleRate: buffer.sampleRate, numberOfChannels: buffer.numberOfChannels, numberOfFrames: buffer.length, timestamp: 0, data: audioBufferToF32Planar(buffer), }); encoder.configure({ codec: "opus", sampleRate: buffer.sampleRate, numberOfChannels: buffer.numberOfChannels, bitrate: 128_000, }); encoder.encode(data); // 将Opus编码帧封装为WebM格式的简易函数 function opusChunksToWebM(chunks: EncodedAudioChunk[], sampleRate: number, channels: number): Blob { // 简化版WebM头,适配Opus单/双声道音频 const webmHeader = new Uint8Array([ 0x1a, 0x45, 0xdf, 0xa3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x53, 0x80, 0x67, 0x01, 0x00, 0x00, 0x00, 0x15, 0x49, 0xa9, 0x66, 0x01, 0x00, 0x00, 0x00, 0x07, 0x42, 0x86, 0x81, 0x01, (sampleRate >> 24) & 0xff, (sampleRate >> 16) & 0xff, (sampleRate >> 8) & 0xff, sampleRate & 0xff, 0x05, 0x42, 0x82, 0x81, 0x01, channels, 0x02, 0x42, 0x87, 0x81, 0x01, 0x01, 0x02, 0x42, 0x85, 0x81, 0x01, 0x01, 0x00, 0x16, 0x54, 0xae, 0x6b, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x4d, 0x80, 0x81, 0x01, 0x01, 0x07, 0x4d, 0x81, 0x81, 0x01, 0x6f, 0x70, 0x75, 0x73, 0x08, 0x4d, 0x82, 0x81, 0x01, 0x00, 0x00, 0x00, 0x00, 0x05, 0x4d, 0x83, 0x81, 0x01, channels, 0x07, 0x4d, 0x84, 0x81, 0x01, (sampleRate >> 24) & 0xff, (sampleRate >> 16) & 0xff, (sampleRate >> 8) & 0xff, sampleRate & 0xff, 0x00, 0x1f, 0x43, 0xb6, 0x75, 0x01, 0x00, 0x00, 0x00, 0x00, ]); const blobParts: BlobPart[] = [webmHeader]; // 封装每个编码帧为WebM Block chunks.forEach(chunk => { const chunkData = new Uint8Array(chunk.byteLength); chunk.copyTo(chunkData); // WebM Block头:轨道ID、时间戳、数据长度 const blockHeader = new Uint8Array([ 0x81, // 轨道ID=1 (chunk.timestamp >> 0) & 0xff, (chunk.timestamp >> 8) & 0xff, (chunk.timestamp >> 16) & 0xff, (chunk.timestamp >> 24) & 0xff, 0x00, // 标志位 (chunkData.length >> 0) & 0xff, (chunkData.length >> 8) & 0xff, (chunkData.length >> 16) & 0xff, ]); blobParts.push(blockHeader, chunkData); }); return new Blob(blobParts, { type: 'audio/webm;codecs=opus' }); } encoder.flush().then(() => { const blob = opusChunksToWebM(encodedChunks, buffer.sampleRate, buffer.numberOfChannels); const player = new Audio(); player.src = URL.createObjectURL(blob); player.play().then( () => console.log("playing!"), (err) => console.error(err) ); player.onended = () => URL.revokeObjectURL(player.src); });
说明
上述封装函数是简化版实现,仅适配基础的单/双声道Opus音频场景。如果需要处理更复杂的情况(比如多声道、动态码率、精确时间戳同步等),建议完善容器生成逻辑或使用成熟的WebM封装工具。
内容的提问来源于stack exchange,提问作者Yanick Rochon
相关产品推荐
相关产品推荐

