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

React前端无法播放ESP32经WebSocket传输的PCM音频流

问题:ESP32通过WebSocket传输PCM音频流到React前端无法播放

我正在开发一个项目,通过WebSocket将ESP32设备的输入音频流传输至Node后端服务器,获取到的是PCM格式的音频数据,并将该音频流发送至React前端。尽管前端能接收到完整的音频数据,但始终无法播放音频。我使用pcm-player npm包来播放音频,控制台无报错且无数据丢失,无法定位问题所在。我也曾尝试过node-speaker模块,但该模块已被废弃。

后端代码(Node.js + ws)

const http = require("http");
const WebSocket = require("ws");

const server = http.createServer((req, res) => {
  res.writeHead(200);
  res.end("Hello, world!");
});

const wss = new WebSocket.Server({ server });

let audioData = [];
let clientMap = new Map();

wss.on("connection", (ws) => {
  console.log("Client connected");
  clientMap.set(ws, {});

  ws.on("message", (data) => {
    console.log(wss.clients);
    audioData.push(data);
    sendAudioData();
  });

  ws.on("close", () => {
    console.log("Client disconnected");
    audioData = [];
  });
});

server.listen(5000, () => {
  console.log("Server running on port 5000");
});

function sendAudioData() {
  if (audioData.length > 0) {
    // broadcasting the audio Data to all clients
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(audioData.join(""));
      }
    });
    audioData.length = 0;
  }
}

前端代码(React + pcm-player)

const App = () => {
  const [player, setPlayer] = useState(null);
  const [isPlaying, setIsPlaying] = useState(false);
  const [ws, setWs] = useState(null);
  let audioData = [];
  useEffect(() => {
    const ws = new WebSocket("ws://localhost:5000");

    ws.onopen = () => {
      console.log("WebSocket connected");
    };

    ws.onmessage = (event) => {
      console.log("getting data from server", event.data);
      audioData.push(event.data);
    };

    ws.onclose = () => {
      console.log("WebSocket disconnected");
      audioData.length = 0;
    };

    setWs(ws);

    return () => {
      ws.close();
    };
  }, []);

  useEffect(() => {
    if (player && isPlaying) {
      player.play();
    } else if (player && !isPlaying) {
      player.pause();
    }
  }, [player, isPlaying]);

  useEffect(() => {
    if (ws && audioData.length > 0 && !player) {
      const pcmData = new Uint8Array(
        audioData
          .join("")
          .split("")
          .map((c) => c.charCodeAt(0))
      );
      const pcmPlayer = new PCMPlayer({
        encoding: "16bitInt",
        channels: 1,
        sampleRate: 44100,
      });
      pcmPlayer.feed(pcmData);
      setPlayer(pcmPlayer);
      setIsPlaying(true);
    }
  }, [ws, audioData, player]);

  const handlePlayClick = () => {
    console.log("playing");
    setIsPlaying(true);
  };

  const handleStopClick = () => {
    console.log("stopping");
    setIsPlaying(false);
  };

  return (
    <div
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        width: "100vw",
        height: "100vh",
        gap: "5px",
      }}
    >
      <button onClick={handlePlayClick}>Play</button>
      <button onClick={handleStopClick}>Stop</button>
    </div>
  );
};

问题分析与解决方案

核心问题

  1. 后端音频数据拼接错误:audioData.join("") 将Buffer对象转为字符串拼接,破坏了PCM二进制数据结构,导致前端解析出错误数据。
  2. 前端数据处理逻辑缺陷:
    • 用普通变量audioData存储音频数据,组件重渲染时会被重置,导致数据丢失。
    • 仅在初始化播放器时喂入一次数据,后续新接收的音频未持续传递给播放器。
    • 字符串转Uint8Array的方式不适用于二进制WebSocket数据,引发编码错误。

修正步骤

1. 修复后端数据广播逻辑

直接拼接Buffer对象,避免转字符串破坏数据:

function sendAudioData() {
  if (audioData.length > 0) {
    const combinedBuffer = Buffer.concat(audioData);
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(combinedBuffer);
      }
    });
    audioData.length = 0;
  }
}

2. 修正前端数据处理与播放逻辑

  • 用useState存储音频数据,避免组件渲染时丢失。
  • 指定WebSocket接收二进制数据类型为arraybuffer。
  • 用useRef存储播放器实例,避免重复创建。
  • 持续将新接收的数据喂给播放器,而非仅初始化一次。
  • 确保PCM参数与ESP32输出完全匹配。

修正后的前端代码:

import { useState, useEffect, useRef } from 'react';
import PCMPlayer from 'pcm-player';

const App = () => {
  const [isPlaying, setIsPlaying] = useState(false);
  const [ws, setWs] = useState(null);
  const playerRef = useRef(null);
  const [audioBufferList, setAudioBufferList] = useState([]);

  useEffect(() => {
    const ws = new WebSocket("ws://localhost:5000");
    ws.binaryType = 'arraybuffer';

    ws.onopen = () => console.log("WebSocket connected");
    ws.onmessage = (event) => setAudioBufferList(prev => [...prev, event.data]);
    ws.onclose = () => {
      console.log("WebSocket disconnected");
      setAudioBufferList([]);
    };

    setWs(ws);
    return () => ws.close();
  }, []);

  useEffect(() => {
    if (!playerRef.current) {
      playerRef.current = new PCMPlayer({
        encoding: "16bitInt",
        channels: 1,
        sampleRate: 44100, // 务必与ESP32输出一致,常见值还有16000、8000
        flushingTime: 100, // 调整刷新时间优化实时性
      });
    }
  }, []);

  useEffect(() => {
    if (playerRef.current) {
      isPlaying ? playerRef.current.play() : playerRef.current.pause();
    }
  }, [isPlaying]);

  useEffect(() => {
    if (playerRef.current && audioBufferList.length > 0) {
      audioBufferList.forEach(buffer => {
        const pcmData = new Int16Array(buffer);
        playerRef.current.feed(pcmData);
      });
      setAudioBufferList([]);
    }
  }, [audioBufferList]);

  return (
    <div
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        width: "100vw",
        height: "100vh",
        gap: "5px",
      }}
    >
      <button onClick={() => setIsPlaying(true)}>Play</button>
      <button onClick={() => setIsPlaying(false)}>Stop</button>
    </div>
  );
};

export default App;

额外注意事项

  • 确认ESP32输出的PCM参数(采样率、位深、声道数)与前端播放器配置完全一致,否则会出现无声或杂音。
  • 前后端分别打印Buffer长度,检查数据是否完整传输,无截断。
  • 若追求低延迟,可适当调小flushingTime参数。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 03:49:54