You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在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>
  );
}

关键注意事项(必看!)

  1. 参数匹配:如果播放还是有噪音,先确认VAPI.AI的PCM参数。你可以用FFmpeg查看之前保存的audio.pcm

    ffmpeg -i audio.pcm
    

    输出里会显示采样率(比如8000 Hz)、位深(16 bit)、声道数(1 channel),把代码里的采样率改成对应的值就行。

  2. 浏览器交互限制:浏览器会自动暂停未经过用户交互的AudioContext,如果你的播放器一开始没声音,可以加个“开始播放”按钮,点击时恢复AudioContext:

    const handleStart = () => {
      audioContextRef.current?.resume();
    };
    
  3. 缓冲优化:如果播放有卡顿,可以调整缓冲策略——比如积累2-3段PCM数据再开始播放,平衡延迟和流畅度。

  4. 错误处理:可以在组件里添加错误提示,比如WebSocket断开时显示“连接已断开”的提示,提升用户体验。

替代方案(如果直接播放有问题)

如果还是遇到兼容性问题,你可以在服务器端用FFmpeg实时转成WebM/MP3流,然后用<audio>标签播放,但这会增加服务器负载,不如直接播放PCM高效。

备注:内容来源于stack exchange,提问作者Abdul Waheed

火山引擎 最新活动