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

React前端替代DecodeAudioData播放WebSocket音频块方案问询

在React前端实现WebSocket流式音频播放

问题核心是DecodeAudioData需要完整的音频文件(如完整MP3/WAV)才能解码,无法处理分片的流式数据。而Node.js的speaker.write直接接收PCM格式数据并播放,前端要实现相同效果,需用Web Audio API的流式处理能力,以下是两种可行方案:

方案一:使用ScriptProcessorNode(兼容旧浏览器)

虽然该API已被标记为弃用,但目前多数浏览器仍支持,适合快速实现:

React组件示例

import { useEffect, useRef, useState } from 'react';

const WebSocketAudioPlayer = () => {
  const audioContextRef = useRef(null);
  const scriptNodeRef = useRef(null);
  const wsRef = useRef(null);
  const audioQueueRef = useRef([]);
  const [isPlaying, setIsPlaying] = useState(false);

  // 初始化音频上下文与WebSocket(需用户交互触发,规避浏览器自动播放限制)
  const initPlayback = () => {
    if (audioContextRef.current) return;

    // 初始化AudioContext,采样率需与服务器发送的音频一致
    const audioContext = new (window.AudioContext || window.webkitAudioContext)({
      sampleRate: 44100
    });
    audioContextRef.current = audioContext;

    // 创建脚本处理器节点:缓冲区大小4096,单声道输入输出
    const scriptNode = audioContext.createScriptProcessor(4096, 1, 1);
    scriptNodeRef.current = scriptNode;

    // 实时填充音频数据
    scriptNode.onaudioprocess = (e) => {
      const outputBuffer = e.outputBuffer.getChannelData(0);
      let offset = 0;

      // 从队列中取数据填充输出缓冲区
      while (offset < outputBuffer.length && audioQueueRef.current.length > 0) {
        const chunk = audioQueueRef.current[0];
        const copyLength = Math.min(chunk.length, outputBuffer.length - offset);
        
        outputBuffer.set(chunk.subarray(0, copyLength), offset);
        offset += copyLength;

        // 处理剩余数据
        if (copyLength < chunk.length) {
          audioQueueRef.current[0] = chunk.subarray(copyLength);
        } else {
          audioQueueRef.current.shift();
        }
      }

      // 剩余位置填充静音
      if (offset < outputBuffer.length) {
        outputBuffer.fill(0, offset);
      }
    };

    scriptNode.connect(audioContext.destination);

    // 初始化WebSocket接收音频
    const ws = new WebSocket('ws://你的服务器地址');
    ws.binaryType = 'arraybuffer';
    wsRef.current = ws;

    ws.onmessage = (event) => {
      if (!audioContextRef.current) return;
      
      // 假设服务器发送16位单声道PCM数据,转换为Web Audio所需的Float32格式(范围-1到1)
      const pcm16 = new Int16Array(event.data);
      const pcm32 = new Float32Array(pcm16.length);
      for (let i = 0; i < pcm16.length; i++) {
        pcm32[i] = pcm16[i] / 32768;
      }

      audioQueueRef.current.push(pcm32);
    };

    setIsPlaying(true);
  };

  // 停止播放并清理资源
  const stopPlayback = () => {
    if (audioContextRef.current) {
      audioContextRef.current.close();
      audioContextRef.current = null;
    }
    if (wsRef.current) {
      wsRef.current.close();
      wsRef.current = null;
    }
    audioQueueRef.current = [];
    setIsPlaying(false);
  };

  // 组件卸载时自动清理
  useEffect(() => {
    return () => stopPlayback();
  }, []);

  return (
    <div>
      <button onClick={isPlaying ? stopPlayback : initPlayback}>
        {isPlaying ? '停止播放' : '开始播放'}
      </button>
    </div>
  );
};

export default WebSocketAudioPlayer;

方案二:使用AudioWorklet(现代浏览器推荐)

AudioWorklet是ScriptProcessorNode的替代方案,性能更好,适合现代浏览器:

步骤1:创建AudioWorklet处理器文件(audio-processor.js)

class StreamAudioProcessor extends AudioWorkletProcessor {
  constructor() {
    super();
    this.audioQueue = [];
    // 接收来自主线程的音频数据
    this.port.onmessage = (event) => {
      this.audioQueue.push(event.data);
    };
  }

  process(inputs, outputs) {
    const outputChannel = outputs[0][0];
    let offset = 0;

    // 填充输出缓冲区
    while (offset < outputChannel.length && this.audioQueue.length > 0) {
      const chunk = this.audioQueue[0];
      const copyLength = Math.min(chunk.length, outputChannel.length - offset);
      
      outputChannel.set(chunk.subarray(0, copyLength), offset);
      offset += copyLength;

      // 处理剩余数据
      if (copyLength < chunk.length) {
        this.audioQueue[0] = chunk.subarray(copyLength);
      } else {
        this.audioQueue.shift();
      }
    }

    // 剩余位置填充静音
    if (offset < outputChannel.length) {
      outputChannel.fill(0, offset);
    }

    return true;
  }
}

registerProcessor('stream-audio-processor', StreamAudioProcessor);

步骤2:React组件中使用AudioWorklet

import { useEffect, useRef, useState } from 'react';

const WebSocketAudioPlayer = () => {
  const audioContextRef = useRef(null);
  const workletNodeRef = useRef(null);
  const wsRef = useRef(null);
  const [isPlaying, setIsPlaying] = useState(false);

  const initPlayback = async () => {
    if (audioContextRef.current) return;

    const audioContext = new (window.AudioContext || window.webkitAudioContext)({
      sampleRate: 44100
    });
    // 加载AudioWorklet模块
    await audioContext.audioWorklet.addModule('/audio-processor.js');
    
    // 创建Worklet节点并连接到输出
    const workletNode = new AudioWorkletNode(audioContext, 'stream-audio-processor');
    workletNode.connect(audioContext.destination);
    
    audioContextRef.current = audioContext;
    workletNodeRef.current = workletNode;

    // 初始化WebSocket
    const ws = new WebSocket('ws://你的服务器地址');
    ws.binaryType = 'arraybuffer';
    wsRef.current = ws;

    ws.onmessage = (event) => {
      if (!audioContextRef.current || !workletNodeRef.current) return;
      
      // 转换PCM格式并发送到Worklet
      const pcm16 = new Int16Array(event.data);
      const pcm32 = new Float32Array(pcm16.length);
      for (let i = 0; i < pcm16.length; i++) {
        pcm32[i] = pcm16[i] / 32768;
      }

      workletNodeRef.current.port.postMessage(pcm32);
    };

    setIsPlaying(true);
  };

  const stopPlayback = () => {
    if (audioContextRef.current) {
      audioContextRef.current.close();
      audioContextRef.current = null;
    }
    if (wsRef.current) {
      wsRef.current.close();
      wsRef.current = null;
    }
    setIsPlaying(false);
  };

  useEffect(() => {
    return () => stopPlayback();
  }, []);

  return (
    <div>
      <button onClick={isPlaying ? stopPlayback : initPlayback}>
        {isPlaying ? '停止播放' : '开始播放'}
      </button>
    </div>
  );
};

export default WebSocketAudioPlayer;

关键注意事项

  1. 音频格式匹配:必须确保前端处理的采样率、声道数、位深与服务器发送的音频完全一致,否则会出现杂音或无法播放。
  2. 自动播放限制:浏览器禁止无用户交互的自动音频播放,必须通过点击按钮等用户操作初始化AudioContext。
  3. 缓冲区管理:避免队列中积累过多数据导致延迟,或队列空导致断音,可根据实际场景调整缓冲区大小。

内容的提问来源于stack exchange,提问作者Harsh Singh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 19:43:17