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

Twilio语音通话中无法听到WebSocket发送的音频消息

问题

我正在开发一个集成Twilio Voice和Google Cloud Speech-to-Text的Node.js WebSocket服务器,目标是通过WebSocket回流传音频消息到Twilio语音通话中,但发送后无法在通话中听到音频。

require("dotenv").config();
const WebSocket = require("ws");
const twilio = require("twilio");
const speech = require("@google-cloud/speech");
const fs = require("fs");
const ffmpeg = require("fluent-ffmpeg");
const clientGoogle = new speech.SpeechClient();

process.env.GOOGLE_APPLICATION_CREDENTIALS = `${__dirname}/seismic-iridium-434511-n8-cc9b62d1aeac.json`;

const accountSid = process.env.ACCOUNTSID;
const authToken = process.env.AUTHTOKEN;
const client = twilio(accountSid, authToken);

const wss = new WebSocket.Server({ port: 8080 });

const request = {
  config: {
    encoding: "MULAW",
    sampleRateHertz: 8000,
    languageCode: "en-GB",
  },
  interimResults: true,
};

wss.on("connection", (ws) => {
  console.log("WebSocket connection established.");

  let recognizeStream;
  let silenceTimeout;

  ws.on("message", (message) => {
    const msg = JSON.parse(message);

    switch (msg.event) {
      case "connected":
        console.log(`A new call has connected.`);
        break;
      case "start":
        console.log(`Starting Media Stream ${msg.streamSid}`);

        sendAudioResponse(ws, msg.streamSid);

        recognizeStream = clientGoogle
          .streamingRecognize(request)
          .on("error", (err) => {
            console.error("Error in recognition stream:", err);
            recognizeStream = null;
          })
          .on("data", (data) => {
            const transcript = data.results[0].alternatives[0].transcript;
            console.log(transcript);
          });
        break;
      case "media":
        if (recognizeStream) {
          recognizeStream.write(msg.media.payload);
        } else {
          console.warn("Media received but no active recognize stream.");
        }
        break;
      case "stop":
        console.log(`Call Has Ended`);
        if (recognizeStream) {
          recognizeStream.destroy();
          recognizeStream = null;
        }
        clearTimeout(silenceTimeout);
        break;
    }
  });

  ws.on("close", () => {
    console.log("WebSocket connection closed.");
    if (recognizeStream) {
      recognizeStream.destroy();
      recognizeStream = null;
    }
    clearTimeout(silenceTimeout);
  });
});

function convertWavToMulaw(wavFilePath, outputFilePath, callback) {
  ffmpeg(wavFilePath)
    .audioCodec("pcm_mulaw")
    .audioFrequency(8000)
    .format("mulaw"); // Explicitly specify the format
    .on("end", function () {
      console.log("Conversion to mulaw completed.");
      callback(null, outputFilePath);
    })
    .on("error", function (err) {
      console.error("Error during conversion:", err);
      callback(err);
    })
    .save(outputFilePath);
}

function removeHeadersAndEncode(mulawFilePath) {
  const audioBuffer = fs.readFileSync(mulawFilePath);
  return audioBuffer.toString("base64"); // Encode the raw audio data to base64
}

function sendAudioResponse(ws, streamSid) {
  const wavFilePath = `${__dirname}/a.wav`;
  const rawFilePath = `${__dirname}/a.raw`; // Use .raw for the output file

  convertWavToMulaw(wavFilePath, rawFilePath, (err) => {
    if (err) {
      console.error("Failed to convert WAV to mulaw.");
      return;
    }

    const base64Payload = removeHeadersAndEncode(rawFilePath);

    ws.send(
      JSON.stringify({
        event: "media",
        streamSid: streamSid,
        media: {
          payload: base64Payload,
        },
      })
    );
  });
}

console.log(
  "WebSocket server is listening on ws://rnhcn-2405-201-682f-e812-e9db-c56d-9ca1-f001.a.free.pinggy.link"
);

async function makeCallWithMediaStream(toNumber, fromNumber) {
  try {
    const call = await client.calls.create({
      to: toNumber,
      from: fromNumber,
      twiml: `<Response>
                <Start>
                  <Stream url="wss://rnhcn-2405-201-682f-e812-e9db-c56d-9ca1-f001.a.free.pinggy.link"/>
                </Start>
                <Say>Hello! This call is being monitored for a demonstration.</Say>
                <Pause length="30" />
              </Response>`,
    });

    console.log(`Call initiated with SID: ${call.sid}`);
  } catch (error) {
    console.error(`Error making call: ${error.message}`);
  }
}

makeCallWithMediaStream("+918888888888", "+19876543210");

根据Twilio文档,media.payload应为原始MULAW/8000音频,经base64编码且不能包含任何音频文件头字节。我已尝试将.wav文件转换为MULAW格式、移除文件头并将原始音频数据编码为base64,但音频消息仍无法在通话中播放。我的实现中遗漏或错误了什么?恳请指导!


解决方案

1. 修复FFmpeg转换代码的语法错误

convertWavToMulaw函数中,.format("mulaw");后的分号破坏了链式调用,导致后续的on事件监听无法绑定到FFmpeg进程,转换过程完全失效。修正后的代码:

function convertWavToMulaw(wavFilePath, outputFilePath, callback) {
  ffmpeg(wavFilePath)
    .audioCodec("pcm_mulaw")
    .audioFrequency(8000)
    .format("mulaw") // 移除此处的分号
    .on("end", function () {
      console.log("Conversion to mulaw completed.");
      callback(null, outputFilePath);
    })
    .on("error", function (err) {
      console.error("Error during conversion:", err);
      callback(err);
    })
    .save(outputFilePath);
}

2. 强制输出纯原始MULAW数据

部分FFmpeg版本使用.format("mulaw")仍会生成带文件头的文件,改用rawvideo格式可确保输出纯原始音频数据,同时强制单声道(Twilio语音通话要求单声道):

function convertWavToMulaw(wavFilePath, outputFilePath, callback) {
  ffmpeg(wavFilePath)
    .audioCodec("pcm_mulaw")
    .audioFrequency(8000)
    .audioChannels(1)
    .format("rawvideo")
    .on("end", function () {
      console.log("Conversion to mulaw completed.");
      callback(null, outputFilePath);
    })
    .on("error", function (err) {
      console.error("Error during conversion:", err);
      callback(err);
    })
    .save(outputFilePath);
}

3. 调整音频发送时机

在start事件触发时立即发送音频,可能此时通话的媒体流尚未完全就绪,建议延迟几百毫秒再发送:

case "start":
  console.log(`Starting Media Stream ${msg.streamSid}`);
  // 延迟500ms确保媒体流建立完成
  setTimeout(() => {
    sendAudioResponse(ws, msg.streamSid);
  }, 500);
  // 剩余代码保持不变

4. 验证转换后的音频数据

将转换后的base64数据解码为.raw文件,用Audacity等工具导入(选择MULAW编码、8000Hz采样率、单声道),确认音频本身可正常播放,排除源文件或转换环节的问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 18:45:06