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

