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

使用Websocket传输MediaRecorder音频块解码失败问题求助

解决MediaRecorder音频块转发后无法解码的问题

我尝试通过WebSocket将MediaRecorder录制的音频块发送至服务器,再转发给其他客户端播放。第一个音频块可正常播放,但后续出现如下错误:

Error decoding audio data: EncodingError: Failed to execute 'decodeAudioData' on 'BaseAudioContext': Unable to decode audio data

原代码实现

录制逻辑

const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mediaRecorder = new MediaRecorder(stream, {
  mimeType: "audio/webm",
});

mediaRecorder.ondataavailable = async (event) => {
  if (event.data.size > 0) {
    const b64 = await blobToBase64(event.data);
    sendMessage({
      payload: {
        base64: b64,
      },
    });
  }
};

mediaRecorder.start(250);

播放逻辑

const arrayBuffer = base64ToArrayBuffer(message.payload.base64);
const audioContext = new AudioContext();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start();

辅助函数

const base64ToArrayBuffer = (base64: string): ArrayBuffer => {
  const binaryString = window.atob(base64);
  const len = binaryString.length;
  const bytes = new Uint8Array(len);
  for (let i = 0; i < len; i++) {
    bytes[i] = binaryString.charCodeAt(i);
  }
  return bytes.buffer;
};

const blobToBase64 = (blob: Blob): Promise<string> => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(blob);
    reader.onloadend = () => resolve(reader.result as string);
    reader.onerror = reject;
  });
};

问题根源

MediaRecorder的ondataavailable事件返回的后续Blob块不包含WebM文件的头部信息,只有第一个块带有完整的文件头。而decodeAudioData只能解码结构完整的音频文件/流,单独的后续块无法被识别解析。

另外,原辅助函数存在隐藏问题:blobToBase64返回的是带data:audio/webm;base64,前缀的字符串,base64ToArrayBuffer直接解码会导致二进制数据错误,这也是后续块解码失败的诱因之一。


修复方案

1. 修正辅助函数(必做)

先去掉Base64字符串的Data URL前缀,再转换为ArrayBuffer:

const base64ToArrayBuffer = (base64: string): ArrayBuffer => {
  // 移除data URL前缀,只保留纯Base64部分
  const pureBase64 = base64.split(',')[1];
  const binaryString = window.atob(pureBase64);
  const len = binaryString.length;
  const bytes = new Uint8Array(len);
  for (let i = 0; i < len; i++) {
    bytes[i] = binaryString.charCodeAt(i);
  }
  return bytes.buffer;
};

2. 选择合适的播放方案

方案A:累积音频块拼接成完整流播放

接收端维护全局Blob对象,将每次收到的音频块追加后统一解码,适合短会话场景:

// 接收端全局变量,累积音频数据
let audioBlob = new Blob([], { type: 'audio/webm' });
const audioContext = new AudioContext();

async function handleAudioMessage(message) {
  const arrayBuffer = base64ToArrayBuffer(message.payload.base64);
  // 追加新的音频块到全局Blob
  audioBlob = new Blob([audioBlob, arrayBuffer], { type: 'audio/webm' });
  
  // 解码完整音频流并播放
  const fullBuffer = await audioBlob.arrayBuffer();
  const audioBuffer = await audioContext.decodeAudioData(fullBuffer);
  
  const source = audioContext.createBufferSource();
  source.buffer = audioBuffer;
  source.connect(audioContext.destination);
  source.start();
}

注意:长时间运行会累积大量数据,占用内存较高。

方案B:使用MediaSource API流式播放(推荐)

MediaSource支持流式解析媒体数据,无需累积全部内容,适合实时音频场景:

// 接收端初始化
const audio = new Audio();
const mediaSource = new MediaSource();
audio.src = URL.createObjectURL(mediaSource);
audio.play();

let sourceBuffer;
mediaSource.addEventListener('sourceopen', () => {
  // 匹配WebM的Opus编码
  sourceBuffer = mediaSource.addSourceBuffer('audio/webm; codecs="opus"');
});

async function handleAudioMessage(message) {
  const arrayBuffer = base64ToArrayBuffer(message.payload.base64);
  // 等待sourceBuffer处于可写入状态
  while (sourceBuffer.updating) {
    await new Promise(resolve => setTimeout(resolve, 10));
  }
  sourceBuffer.appendBuffer(arrayBuffer);
}

方案C:更换编码格式

如果必须单独解码每个块,可改用无容器的audio/wav格式(但体积大,不适合实时传输):

// 录制端修改mimeType
const mediaRecorder = new MediaRecorder(stream, {
  mimeType: "audio/wav",
});

接收端可保留原解码逻辑,每个WAV块可独立解析。


内容的提问来源于stack exchange,提问作者Michał Frączek

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 10:23:24