如何在Next.js前端直接播放WebSocket传输的PCM音频流,无需先保存到文件?
如何在Next.js前端直接播放WebSocket传输的PCM音频流,无需先保存到文件?
嘿,我完全懂你的需求——不想绕服务器存文件转格式的弯路,直接在Next.js前端把WebSocket传过来的PCM音频实时播放出来,之前试的方法出噪音对吧?这大概率是因为你没和VAPI.AI的PCM流参数匹配上,咱们一步步来解决这个问题:
核心问题:PCM参数不匹配导致噪音
PCM是裸音频数据,播放时必须严格匹配它的采样率、位深、声道数,如果这些参数和Web Audio API的设置对不上,出来的就是噪音。VAPI.AI的电话音频流一般是:16位有符号整数、单声道、采样率8kHz或16kHz,咱们先按这个标准来配置。
直接在Next.js前端实现的方案
下面给你一个完整的客户端组件示例,用Web Audio API来实时处理并播放PCM流:
'use client'; import { useEffect, useRef } from 'react'; export default function PCMAudioPlayer() { const audioContextRef = useRef<AudioContext | null>(null); const wsRef = useRef<WebSocket | null>(null); const audioBufferQueueRef = useRef<Float32Array[]>([]); const isPlayingRef = useRef(false); useEffect(() => { // 初始化Web Audio上下文 const initAudioContext = () => { // 兼容不同浏览器的AudioContext const AudioContextConstructor = window.AudioContext || (window as any).webkitAudioContext; // 这里的采样率要和VAPI.AI的流一致,先试16kHz,有噪音就改成8kHz audioContextRef.current = new AudioContextConstructor({ sampleRate: 16000 }); }; // 把收到的16位PCM数据转换成Web Audio需要的格式 const processPCMData = (data: ArrayBuffer) => { if (!audioContextRef.current) return; // VAPI.AI的PCM是16位有符号整数,先转成Int16Array const int16Data = new Int16Array(data); // 转成Web Audio要求的Float32Array(范围-1到1) const float32Data = new Float32Array(int16Data.length); for (let i = 0; i < int16Data.length; i++) { float32Data[i] = int16Data[i] / 32768; // 16位有符号数的最大值是32767,除以32768得到标准范围 } // 把转换好的数据加入播放队列 audioBufferQueueRef.current.push(float32Data); // 如果当前没在播放,就启动播放流程 if (!isPlayingRef.current) { playQueuedAudio(); } }; // 播放队列里的音频片段 const playQueuedAudio = () => { if (!audioContextRef.current || audioBufferQueueRef.current.length === 0) { isPlayingRef.current = false; return; } isPlayingRef.current = true; // 取出队列里的第一段数据 const currentData = audioBufferQueueRef.current.shift()!; // 创建AudioBuffer,参数要和流匹配:单声道、数据长度、采样率 const audioBuffer = audioContextRef.current.createBuffer( 1, currentData.length, audioContextRef.current.sampleRate ); audioBuffer.getChannelData(0).set(currentData); // 创建音频源节点并播放 const audioSource = audioContextRef.current.createBufferSource(); audioSource.buffer = audioBuffer; audioSource.connect(audioContextRef.current.destination); audioSource.start(); // 当前片段播放完后,自动播放下一段 audioSource.onended = () => { playQueuedAudio(); }; }; // 初始化WebSocket连接 const initWebSocket = () => { const ws = new WebSocket("wss://aws-us-west-2-production1-phone-call-websocket.vapi.ai/7420f27a-30fd-4f49-a995-5549ae7cc00d/transport"); wsRef.current = ws; ws.onopen = () => { console.log('WebSocket连接已建立'); // 浏览器会自动挂起未交互的AudioContext,这里恢复它 if (audioContextRef.current?.state === 'suspended') { audioContextRef.current.resume(); } }; ws.onmessage = (event) => { if (event.data instanceof ArrayBuffer) { // 处理二进制PCM数据 processPCMData(event.data); } else { // 处理非二进制的状态消息 console.log('收到状态消息:', JSON.parse(event.data)); } }; ws.onerror = (error) => { console.error('WebSocket出错:', error); isPlayingRef.current = false; }; ws.onclose = () => { console.log('WebSocket连接已关闭'); isPlayingRef.current = false; }; }; // 初始化音频上下文和WebSocket initAudioContext(); initWebSocket(); // 组件卸载时清理资源 return () => { wsRef.current?.close(); audioContextRef.current?.close(); }; }, []); return ( <div className="p-4"> <h3 className="text-lg font-medium">实时通话音频播放器</h3> <p className="text-gray-600 mt-2">正在接收并播放音频流...</p> </div> ); }
关键注意事项(必看!)
参数匹配:如果播放还是有噪音,先确认VAPI.AI的PCM参数。你可以用FFmpeg查看之前保存的
audio.pcm:ffmpeg -i audio.pcm输出里会显示采样率(比如
8000 Hz)、位深(16 bit)、声道数(1 channel),把代码里的采样率改成对应的值就行。浏览器交互限制:浏览器会自动暂停未经过用户交互的AudioContext,如果你的播放器一开始没声音,可以加个“开始播放”按钮,点击时恢复AudioContext:
const handleStart = () => { audioContextRef.current?.resume(); };缓冲优化:如果播放有卡顿,可以调整缓冲策略——比如积累2-3段PCM数据再开始播放,平衡延迟和流畅度。
错误处理:可以在组件里添加错误提示,比如WebSocket断开时显示“连接已断开”的提示,提升用户体验。
替代方案(如果直接播放有问题)
如果还是遇到兼容性问题,你可以在服务器端用FFmpeg实时转成WebM/MP3流,然后用<audio>标签播放,但这会增加服务器负载,不如直接播放PCM高效。
备注:内容来源于stack exchange,提问作者Abdul Waheed




