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> ); };
问题分析与解决方案
核心问题
- 后端音频数据拼接错误:
audioData.join("")将Buffer对象转为字符串拼接,破坏了PCM二进制数据结构,导致前端解析出错误数据。 - 前端数据处理逻辑缺陷:
- 用普通变量
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
相关产品推荐
相关产品推荐

