테스트

발행: (2026년 3월 9일 AM 07:04 GMT+9)
10 분 소요
원문: 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.

Participants: 5–7명의 발신자 + 1명의 호스트
Core Issue: Gather는 “을 감쌀 수 없습니다
Recommended Production Path: Media Streams + DTMF 감지
System Feasibility: ✓ 여전히 완전히 실현 가능

01 ❌ 근본 문제

Twilio의 TwiML 사양은 통화가 “ 안에 있을 때 DTMF 감지를 허용하지 않습니다. 참가자가 누른 키패드 숫자는 오디오 톤으로 컨퍼런스 룸에 전송되며 서버‑사이드 웹훅이 발생하지 않습니다. 이를 위한 기본 이벤트도 없습니다.

원본 문서에서는 안에 를 중첩하는 방식을 제안했지만, 이는 구조적으로 유효하지 않음을 의미합니다. Twilio TwiML 스키마는 엄격한 부모‑자식 규칙을 강제합니다:

TwiML Verb유효한 자식비고
, , “DTMF 또는 음성 입력을 수집
, , , , “ 안에 들어가는 명사
자식이 없음. “ 안에 있을 수 없음

Twilio Node.js SDK도 이 스키마를 강제합니다. Gather 혹은 VoiceResponse 객체에서 .conference() 를 호출하면 즉시 오류가 발생합니다:

// TypeError: twiml.conference is not a function

⚠️ statusCallbackEvent 수정

statusCallbackEvent 매개변수에는 “unmute”(음소거 해제) 이벤트가 포함되지 않습니다. 유효한 값은 다음과 같습니다:

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

Twilio는 participant-mute 이벤트를 음소거와 음소거 해제 모두에 대해 발생시킵니다. 웹훅 본문의 Muted 필드(true 또는 false)가 어떤 동작이 일어났는지를 알려줍니다.

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

02 Proof‑of‑Concept (POC) – TwiML 제약을 준수하는 두 접근법

접근법 A – 회의 전 사전 수집

  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는 사용 가능한 POC 솔루션이지만, 실제 운영에서는 Media Streams(섹션 03)로 업그레이드하여 참가자 주도 손들기를 구현해야 합니다.

03 Alternative 1 – Media Streams + Goertzel (Production‑Grade)

목표: 회의에서 참가자를 아예 끊어내지 않고 실시간으로 DTMF 톤을 감지합니다.

작동 방식

  1. Twilio Media Streams가 각 참가자의 통화에서 원시 오디오 스트림(8 kHz µ‑law)을 서버의 WebSocket 엔드포인트로 전송합니다.
  2. 서버에서 Goertzel 기반 DTMF 감지기가 들어오는 오디오를 처리합니다.
  3. 키가 눌리면 톤이 서버 측에서 감지되고 손들기 이벤트가 생성됩니다.
Participant presses *1

Phone keypad → Twilio streams audio (8 kHz µ-law) via WebSocket

Your WS server → Goertzel detector

Hand‑raise event → Dashboard notified

샘플 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 Alternative 3 – Twilio Sync State

(세부 사항은 간략히 생략되었습니다 – 전체 설명은 원본 문서를 참조하십시오.)

06 대안 4 – 컨퍼런스 보류 + 수집

(간략히 생략 – 전체 설명은 원본 문서를 참조하십시오.)

07 대안 5 – 듀얼‑채널 (전화 + 웹)

(간략히 생략 – 전체 설명은 원본 문서를 참조하십시오.)

08 Decision Matrix & Recommendation

ArchitectureAudio InterruptionParticipant‑InitiatedImplementation EffortCost
Gather‑Before‑Conference✅ 없음❌ 아니오 (사전 참여만)낮음무료
Host‑Initiated Redirect⚠️ 짧은 일시정지❌ 아니오 (호스트 전용)중간무료
Media Streams + Goertzel✅ 없음✅ 예높음 (WebSocket + DSP)무료 (음성 전용)
Flex / TaskRouter다양함다양함높음유료 (Flex)
Sync State다양함다양함중간유료 (Sync)
Conference Hold + Gather⚠️ 일시정지❌ 호스트 전용중간무료
Dual‑Channel✅ 없음✅ 예 (웹)높음유료 (Web UI)

Recommendation:
프로덕션 수준의 중단 없음, 참가자 주도 손들기 경험을 위해 대안 1 – Media Streams + Goertzel을 채택하십시오. 사전 참여 손들기에 대한 빠른 대체 방안으로만 Gather‑Before‑Conference 패턴을 사용하십시오.

켜짐

  • 더 높은 서버 자원 사용 (참가자당 오디오 처리)
  • 다자리 디바운싱 로직 필요 (*1 = 두 개 신호)
0 조회
Back to Blog

관련 글

더 보기 »