//! DTMF detection — parses RFC 2833 telephone-event RTP packets. //! //! Deduplicates repeated packets (same digit sent multiple times with //! increasing duration) and fires once per detected digit. //! //! Ported from ts/call/dtmf-detector.ts. use crate::ipc::{emit_event, OutTx}; /// RFC 2833 event ID → character mapping. const EVENT_CHARS: &[char] = &[ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#', 'A', 'B', 'C', 'D', ]; /// Safety timeout: report digit if no End packet arrives within this many ms. const SAFETY_TIMEOUT_MS: u64 = 200; /// DTMF detector for a single RTP stream. pub struct DtmfDetector { /// Negotiated telephone-event payload type (default 101). telephone_event_pt: u8, /// Clock rate for duration calculation (default 8000 Hz). clock_rate: u32, /// Call ID for event emission. call_id: String, // Deduplication state. current_event_id: Option, current_event_ts: Option, current_event_reported: bool, current_event_duration: u16, out_tx: OutTx, } impl DtmfDetector { pub fn new(call_id: String, out_tx: OutTx) -> Self { Self { telephone_event_pt: 101, clock_rate: 8000, call_id, current_event_id: None, current_event_ts: None, current_event_reported: false, current_event_duration: 0, out_tx, } } /// Feed an RTP packet. Checks PT; ignores non-DTMF packets. /// Returns Some(digit_char) if a digit was detected. pub fn process_rtp(&mut self, data: &[u8]) -> Option { if data.len() < 16 { return None; // 12-byte header + 4-byte telephone-event minimum } let pt = data[1] & 0x7F; if pt != self.telephone_event_pt { return None; } let marker = (data[1] & 0x80) != 0; let rtp_timestamp = u32::from_be_bytes([data[4], data[5], data[6], data[7]]); // Parse telephone-event payload. let event_id = data[12]; let end_bit = (data[13] & 0x80) != 0; let duration = u16::from_be_bytes([data[14], data[15]]); if event_id as usize >= EVENT_CHARS.len() { return None; } // Detect new event. let is_new = marker || self.current_event_id != Some(event_id) || self.current_event_ts != Some(rtp_timestamp); if is_new { // Report pending unreported event. let pending = self.report_pending(); self.current_event_id = Some(event_id); self.current_event_ts = Some(rtp_timestamp); self.current_event_reported = false; self.current_event_duration = duration; if pending.is_some() { return pending; } } if duration > self.current_event_duration { self.current_event_duration = duration; } // Report on End bit (first time only). if end_bit && !self.current_event_reported { self.current_event_reported = true; let digit = EVENT_CHARS[event_id as usize]; let duration_ms = (self.current_event_duration as f64 / self.clock_rate as f64) * 1000.0; emit_event( &self.out_tx, "dtmf_digit", serde_json::json!({ "call_id": self.call_id, "digit": digit.to_string(), "duration_ms": duration_ms.round() as u32, "source": "rfc2833", }), ); return Some(digit); } None } /// Report a pending unreported event. fn report_pending(&mut self) -> Option { if let Some(event_id) = self.current_event_id { if !self.current_event_reported && (event_id as usize) < EVENT_CHARS.len() { self.current_event_reported = true; let digit = EVENT_CHARS[event_id as usize]; let duration_ms = (self.current_event_duration as f64 / self.clock_rate as f64) * 1000.0; emit_event( &self.out_tx, "dtmf_digit", serde_json::json!({ "call_id": self.call_id, "digit": digit.to_string(), "duration_ms": duration_ms.round() as u32, "source": "rfc2833", }), ); return Some(digit); } } None } /// Process a SIP INFO message body for DTMF. pub fn process_sip_info(&mut self, content_type: &str, body: &str) -> Option { let ct = content_type.to_ascii_lowercase(); if ct.contains("application/dtmf-relay") { // Format: "Signal= 5\r\nDuration= 160\r\n" let signal = body .lines() .find(|l| l.to_ascii_lowercase().starts_with("signal")) .and_then(|l| l.split('=').nth(1)) .map(|s| s.trim().to_string())?; if signal.len() != 1 { return None; } let digit = signal.chars().next()?.to_ascii_uppercase(); if !"0123456789*#ABCD".contains(digit) { return None; } emit_event( &self.out_tx, "dtmf_digit", serde_json::json!({ "call_id": self.call_id, "digit": digit.to_string(), "source": "sip-info", }), ); return Some(digit); } if ct.contains("application/dtmf") { let digit = body.trim().chars().next()?.to_ascii_uppercase(); if !"0123456789*#ABCD".contains(digit) { return None; } emit_event( &self.out_tx, "dtmf_digit", serde_json::json!({ "call_id": self.call_id, "digit": digit.to_string(), "source": "sip-info", }), ); return Some(digit); } None } }