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

使用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不符合规范,因此报错。

具体修复步骤:

  1. 先收集所有编码后的EncodedAudioChunk,而不是直接写入ArrayBuffer
  2. 实现WebM容器封装逻辑,将Opus帧打包成符合WebM规范的字节流
  3. 用封装后的字节流生成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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 07:48:11