Node.js实时转码WebSocket传输的Opus音频至PCM报错,求可行方案
实时转码Chrome录制的WebM/Opus音频为PCM的解决方案
需求
将Chrome端通过MediaRecorder录制的audio/webm; codecs=opus音频,经WebSocket传输至服务器后,实时转码为PCM格式转发至其他服务。
客户端实现代码
let recorder: MediaRecorder; socket.on('connect', () => { navigator.mediaDevices .getUserMedia({ audio: { sampleRate: 48000, channelCount: 1, }, video: false, }) .then(async (stream) => { const options = { audioBitsPerSecond: 256000, mimeType: 'audio/webm; codecs=opus', }; recorder = new MediaRecorder(stream, options); recorder.ondataavailable = async (e) => { if (e.data.size <= 0) { return false; } const buffArray = await e.data.arrayBuffer(); socket.emit('audioStream', new Uint8Array(buffArray)); }; recorder.start(20); // 每20ms生成一个分片 }) })
此前尝试的问题
1. 使用audify/node-opus直接解码
代码:
ws.on('audioStream', message => { const buffer = Buffer.isBuffer(message) ? message : Buffer.from(message); const decoder = new OpusDecoder(48000, 1); const frameSize = 960; // 20ms对应帧大小 const decoded = decoder.decode(buffer, frameSize); // 后续PCM处理逻辑 })
报错:TypeError: The compressed data passed is corrupted
原因:MediaRecorder输出的是WebM容器包裹的Opus数据,而非裸Opus帧,直接用Opus解码器无法解析容器结构。
2. 使用prism-media但每次新建解复用器
代码:
ws.on('audioStream', message => { const buffer = Buffer.isBuffer(message) ? message : Buffer.from(message); const demuxer = new prism.opus.WebmDemuxer(); const decodingStream = new prism.opus.Decoder({ rate: 48000, channels: 1, frameSize: 960, }); const decoded = new Promise((resolve, reject) => { demuxer.pipe(decodingStream).on('data', (chunk) => resolve(chunk)); demuxer.write(buffer); }); })
报错:Did not find the EBML tag at the start of the stream
原因:WebM的EBML文件头仅存在于第一个分片,后续分片无头部信息。每次新建解复用器会丢失上下文,无法识别后续分片。
可行转码方案
方案一:FFmpeg流式处理(推荐,跨语言通用)
利用FFmpeg成熟的容器解复用与解码能力,通过标准输入输出实现流式处理。以Node.js为例:
const { spawn } = require('child_process'); // 启动FFmpeg进程,配置输入输出参数 const ffmpegProcess = spawn('ffmpeg', [ '-i', 'pipe:0', // 从标准输入读取WebM流 '-f', 's16le', // 输出16位小端PCM格式 '-ar', '48000', // 匹配客户端采样率 '-ac', '1', // 单声道 '-vn', // 忽略视频(无视频时可省略) '-loglevel', 'error', // 仅输出错误日志 'pipe:1' // 输出PCM到标准输出 ]); // 监听FFmpeg输出的PCM数据,转发至其他服务 ffmpegProcess.stdout.on('data', (pcmChunk) => { // 此处添加发送PCM至目标服务的逻辑 }); // 处理WebSocket接收的音频分片,写入FFmpeg标准输入 ws.on('audioStream', (message) => { const buffer = Buffer.isBuffer(message) ? message : Buffer.from(message); ffmpegProcess.stdin.write(buffer); }); // 处理FFmpeg错误日志 ffmpegProcess.stderr.on('data', (errData) => { console.error(`FFmpeg错误:${errData.toString()}`); });
方案二:复用prism-media的解复用器与解码器
复用全局的解复用器和解码器,保留WebM容器的解析上下文:
const prism = require('prism-media'); // 全局初始化解复用器与解码器,仅创建一次 const webmDemuxer = new prism.opus.WebmDemuxer(); const opusDecoder = new prism.opus.Decoder({ rate: 48000, channels: 1, frameSize: 960, }); // 提前建立管道,监听解码后的PCM数据 webmDemuxer.pipe(opusDecoder).on('data', (pcmChunk) => { // 此处添加发送PCM至目标服务的逻辑 }); // 接收WebSocket分片,写入解复用器 ws.on('audioStream', (message) => { const buffer = Buffer.isBuffer(message) ? message : Buffer.from(message); webmDemuxer.write(buffer); });
方案三:使用libav绑定(如node-av)
通过node-av调用libav库处理流式转码,需提前安装libav依赖:
const av = require('av'); av.registerAll(); // 创建流式输入 const inputStream = av.Input.fromBufferStream(); // 获取音频流 const audioStream = inputStream.streams.find(stream => stream.type === 'audio'); // 配置解码与输出格式 audioStream.codec.context.setCodec('opus'); audioStream.pipe(new av.format.S16LEAudio({ sampleRate: 48000, channels: 1 })) .on('data', (pcmChunk) => { // 此处添加发送PCM至目标服务的逻辑 }); // 写入WebSocket接收的分片 ws.on('audioStream', (message) => { const buffer = Buffer.isBuffer(message) ? message : Buffer.from(message); inputStream.write(buffer); });
内容的提问来源于stack exchange,提问作者Alfchee
相关产品推荐
相关产品推荐

