Vue项目WebSocket音频流在Safari下audioContext解码失败求助
Vue WebSocket音频流在Safari中随机解码失败的解决方案
问题场景
开发的Vue应用通过WebSocket接收分块音频流,实现队列播放逻辑,Chrome中运行完全正常,但在Safari浏览器里,decodeAudioData会随机抛出错误:The buffer passed to decodeAudioData contains an unknown content type,导致播放中断。
原实现代码如下:
vm.socket = new WebSocket('wss://my_website.com'); //required to receive data in array buffer type. If not mentioned I get Blob's vm.socket.binaryType = 'arraybuffer' vm.socket.addEventListener("message", (event) => { const audioData = new Uint8Array(event.data); this.audioQueue.push(audioData); if (!this.isPlaying) { this.playNextChunk(); } }); //Main method which is responsible to process incoming audio chunks async playNextChunk() { if (this.audioQueue.length === 0) { this.isPlaying = false; return; } this.isPlaying = true; const audioData = this.audioQueue.shift(); try { //decode audio data (This fails randomly in between in Safari browser) const audioBuffer = this.audioContext.decodeAudioData(audioData.buffer); const source = this.audioContext.createBufferSource(); source.buffer = audioBuffer; source.connect(this.audioContext.destination); source.onended = this.playNextChunk; source.start(); } catch (error) { console.error('Error decoding audio data', error); } }
问题根源
- 异步处理错误:
decodeAudioData是异步方法,返回Promise,原代码未加await直接赋值给source.buffer,此时audioBuffer是Promise对象而非解码后的音频缓冲区,会导致播放逻辑混乱。 - 音频块完整性问题:Safari对音频帧的完整性要求远高于Chrome,WebSocket传输的分块可能刚好截断了音频帧,Chrome可以容错处理,但Safari无法识别不完整的音频数据,直接解码失败。
修复方案
1. 正确处理异步解码逻辑
必须用await等待decodeAudioData完成,确保拿到有效的音频缓冲区后再创建播放源。
2. 缓存拼接不完整音频帧
新增缓存缓冲区,将接收到的音频分块持续拼接,尝试解码,成功则说明是完整帧,加入播放队列;失败则继续等待下一块数据,直到凑成完整可解码单元。
修复后的完整代码:
vm.socket = new WebSocket('wss://my_website.com'); vm.socket.binaryType = 'arraybuffer'; // 缓存不完整的音频数据,用于拼接成完整帧 let audioFrameCache = new Uint8Array(); vm.socket.addEventListener("message", (event) => { const incomingChunk = new Uint8Array(event.data); // 拼接新接收的分块到缓存 audioFrameCache = mergeUint8Arrays(audioFrameCache, incomingChunk); // 尝试处理缓存中的数据 tryProcessCachedAudio(); }); // 工具函数:合并两个Uint8Array function mergeUint8Arrays(first, second) { const merged = new Uint8Array(first.length + second.length); merged.set(first, 0); merged.set(second, first.length); return merged; } // 尝试解码缓存中的音频数据 async function tryProcessCachedAudio() { if (this.isPlaying) return; // 正在播放时暂不处理,避免队列混乱 try { // 尝试解码缓存数据 const decodedBuffer = await this.audioContext.decodeAudioData(audioFrameCache.buffer); // 解码成功,说明是完整帧,加入播放队列 this.audioQueue.push(decodedBuffer); // 清空缓存,准备接收下一组数据 audioFrameCache = new Uint8Array(); // 未在播放则启动播放流程 if (!this.isPlaying) { this.playNextChunk(); } } catch (err) { // 解码失败,说明缓存数据不完整,等待下一块数据 console.debug("音频帧不完整,等待后续分块"); } } // 播放队列处理方法 async playNextChunk() { if (this.audioQueue.length === 0) { this.isPlaying = false; // 播放完队列后,再尝试一次缓存数据处理 tryProcessCachedAudio(); return; } this.isPlaying = true; const audioBuffer = this.audioQueue.shift(); try { const source = this.audioContext.createBufferSource(); source.buffer = audioBuffer; source.connect(this.audioContext.destination); // 播放结束后标记为未播放,继续处理下一块 source.onended = () => { this.isPlaying = false; this.playNextChunk(); }; source.start(); } catch (err) { console.error("播放音频块失败", err); this.isPlaying = false; this.playNextChunk(); } }
额外Safari兼容提示
- 确保
audioContext是在用户交互(如点击按钮)后初始化,Safari要求音频上下文必须由用户触发才能激活,否则会处于暂停状态。 - 确认服务端传输的音频编码是Safari支持的格式(如AAC、MP3),且分块时尽量对齐音频帧边界,减少不完整帧的出现概率。
内容的提问来源于stack exchange,提问作者Rutvik D Mehta
相关产品推荐
相关产品推荐

