测试

发布: (2026年3月9日 GMT+8 06:04)
8 分钟阅读
原文: Dev.to

Source: Dev.to

DTMF Hand‑Raise System – Corrected technical research: feasibility issues with the original approach, verified working patterns, 5 alternative architectures, and an implementation‑recommendation matrix for the muted‑conference hand‑raise use case.

参与者: 5–7 位来电者 + 1 位主持人
核心问题: Gather 无法包装 “
推荐的生产路径: 媒体流 + DTMF 检测
系统可行性: ✓ 仍然完全可行

01 ❌ 根本问题

Twilio 的 TwiML 规范 不允许在 “ 中的来电者进行 DTMF 检测。参与者按下的任何键盘数字都会作为音频音调发送到会议室——不会触发服务器端 webhook。此场景没有原生事件。

原始文档提议在 中嵌套。这 在结构上是无效的。Twilio TwiML 架构强制严格的父子关系规则:

TwiML 动词有效子元素备注
, , “收集 DTMF 或语音输入
, , , , “ 内的名词
没有子元素。不能放在 “ 中

Twilio Node.js SDK 也遵循此架构。在 GatherVoiceResponse 对象上调用 .conference() 会立即抛出错误:

// TypeError: twiml.conference is not a function

⚠️ statusCallbackEvent 修正

statusCallbackEvent 参数 不包含 “unmute” 事件。有效值为:

start, end, join, leave, mute, hold, modify, speaker, announcement

Twilio 会为 静音和取消静音 两种操作都触发 participant-mute 事件。Webhook 请求体中的 Muted 字段(truefalse)指示具体是哪一种操作。

if (StatusCallbackEvent === 'participant-mute') {
  // Muted === true  → 静音
  // Muted === false → 取消静音
  res.sendStatus(200);
}

02 概念验证(POC)– 两种符合 TwiML 限制的方案

方案 A – 会议前收集(Gather‑Before‑Conference)

  1. 参与者在加入会议前会得到一个短暂的 “ 窗口。
  2. 如果他们在此窗口期间按下 *1,举手操作将在 进入前 被记录。

优点:

  • ✅ 零音频中断
  • ⚡ 仅一次

方案 B – REST API 调用重定向(主持人发起)

  1. 主持人在仪表板上触发一次会议中 DTMF 提示。
  2. 后端调用 twilioClient.calls(callSid).update({ url: gatherUrl }),暂时将参与者拉出会议以收集按键,然后再将其返回。

优点:

  • ✅ 支持通话中操作
  • ⚡ 主持人发起
twiml.say({ voice: 'Polly.Joanna' }, 'Press *1 to raise hand.');
// Gather window — participant can press *1 NOW
// Fall‑through: no key pressed → enter conference muted
twiml.redirect(`${BASE_URL}/webhooks/conference`);
twiml.play(`${BASE_URL}/hold-music`);
res.type('text/xml').send(twiml.toString());

// Handle pre‑join keypress
if (Digits === '*1') {
  // Either way, enter the conference
  res.type('text/xml').send(twiml.toString());
}

仪表板为每位参与者显示一个 “Prompt Hand Raise” 按钮。点击后,后端会将该呼叫的活动通话重定向到一个收集 TwiML 页面,收集响应后再返回会议。

// /voice/gather-hand-raise – the gather prompt TwiML page
const gather = twiml.gather({
  action: `${BASE_URL}/handle-hand-raise`,
  method: 'POST',
  timeout: 5,
  numDigits: 2,
});
gather.say('Press *1 to raise your hand.');
twiml.redirect(`${BASE_URL}/webhooks/conference`);
res.type('text/xml').send(twiml.toString());

// Process their response
if (Digits === '*1') {
  const dial = twiml.dial();
  dial.conference('myConference', { muted: true });
}

ℹ️ 模式 B 的权衡

  • 参与者在收集提示播放期间会 短暂断开 会议音频(约 3‑5 秒)。
  • 这是 主持人发起 的方式——参与者无法在会议内部自行触发。

模式 B 是可用的概念验证方案,但 生产环境应升级到媒体流(第 03 节),以实现真正的参与者主动举手功能。

Source:

03 Alternative 1 – 媒体流 + Goertzel(生产级)

目标: 实时检测 DTMF 音调 且永不将参与者踢出会议

工作原理

  1. Twilio Media Streams 将每个参与者通话的原始音频流(8 kHz µ‑law)发送到你服务器上的 WebSocket 端点。
  2. 你的服务器对收到的音频运行 基于 Goertzel 的 DTMF 检测器
  3. 当按键被按下时,音调在服务器端被检测到,并生成举手事件。
参与者按下 *1

电话键盘 → Twilio 通过 WebSocket 流式传输音频 (8 kHz µ-law)

你的 WS 服务器 → Goertzel 检测器

举手事件 → 仪表盘收到通知

示例 WebSocket 服务器(Node.js)

const WebSocket = require('ws');
const mediaWss = new WebSocket.Server({ path: '/media-stream', server });

mediaWss.on('connection', (ws) => {
  let callSid = null;

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

    if (data.event === 'start') {
      callSid = data.start.callSid; // map stream → Call SID
    }

    if (data.event === 'media') {
      // Decode base64 µ-law audio payload
      const audio = Buffer.from(data.media.payload, 'base64');

      // Run Goertzel DTMF detector on this audio chunk
      const digit = detector.detect(audio);

      if (digit) {
        handleDTMFDigit(callSid, digit);
      }
    }

    if (data.event === 'stop') {
      detector.reset();
    }
  });
});

async function handleDTMFDigit(callSid, digit) {
  // Your business logic – e.g., flag hand‑raise in DB, push to dashboard, etc.
}

DTMF 检测库

语言备注
node-dtmfNode.js简单的 µ‑law 音频 Goertzel 实现
goertzel-jsNode.js低层 Goertzel 滤波器;需要自行映射 DTMF 频率
dtmf-decoderPython若后端使用 Python/FastAPI 时的不错选择
librosa + customPython可能有点大材小用,但非常精准

优缺点

✅ 优点❌ 缺点
参与者保持在会议中 – 零音频中断需要 WebSocket 服务器来接收音频
真正的参与者主动举手,随时可用需要 DTMF 检测库 + Goertzel 实现
无额外 Twilio 成本 – Media Streams 已包含在 Voice 中工程工作量稍高
适用于你定义的任何键盘组合

04 替代方案 2 – Twilio Flex / TaskRouter

(为简洁起见,省略了详细信息——完整描述请参见原始文档。)

05 备选方案 3 – Twilio Sync State

(为简洁起见,省略了细节——请参阅原始文档获取完整描述。)

06 备选方案 4 – 会议保持 + 收集

(为简洁起见省略细节 – 请参阅原始文档获取完整描述。)

07 Alternative 5 – Dual‑Channel (Phone + Web)

(Details omitted for brevity – see original document for full description.)

08 决策矩阵与推荐

架构音频中断参与者发起实现工作量成本
Gather‑Before‑Conference✅ 无❌ 否(仅预加入)Low免费
Host‑Initiated Redirect⚠️ 短暂停顿❌ 否(仅主持人)Medium免费
Media Streams + Goertzel✅ 无✅ 是High(WebSocket + DSP)免费(仅语音)
Flex / TaskRouterVariesVariesHigh付费(Flex)
Sync StateVariesVariesMedium付费(Sync)
Conference Hold + Gather⚠️ 短暂停顿❌ 主持人专用Medium免费
Dual‑Channel✅ 无✅ 是(Web)High付费(Web UI)

推荐:
对于生产级、零中断、参与者发起的举手体验,采用 Alternative 1 – Media Streams + Goertzel。仅在预加入举手时将 Gather‑Before‑Conference 模式作为快速回退使用。

关于

  • 更高的服务器资源使用(每位参与者的音频处理)
  • 需要多位去抖动逻辑(*1 = 两个信号)
0 浏览
Back to Blog

相关文章

阅读更多 »