pub const DTMF_TELEPHONE_EVENT_PT: u8 = 101;
pub const DTMF_CLOCK_RATE: u32 = 8000;
pub const DTMF_EVENT_DURATION_INC: u16 = 160;
const DTMF_DIGITS: &[char] = &[
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#', 'A', 'B', 'C', 'D',
];
pub fn digit_to_event(digit: char) -> Option<u8> {
DTMF_DIGITS.iter().position(|&d| d == digit).map(|i| i as u8)
}
pub fn event_to_digit(event: u8) -> Option<char> {
DTMF_DIGITS.get(event as usize).copied()
}
#[derive(Debug, Clone)]
pub struct DtmfEvent {
pub event: u8,
pub end: bool,
pub volume: u8,
pub duration: u16,
}
pub const DTMF_PAYLOAD_SIZE: usize = 4;
pub fn encode_dtmf(ev: DtmfEvent) -> [u8; 4] {
let mut buf = [0u8; 4];
let e_bit = if ev.end { 0x80u8 } else { 0 };
buf[0] = ev.event;
buf[1] = e_bit | (ev.volume & 0x3F);
buf[2] = (ev.duration >> 8) as u8;
buf[3] = (ev.duration & 0xFF) as u8;
buf
}
pub fn decode_dtmf(data: &[u8]) -> Option<DtmfEvent> {
if data.len() < 4 {
return None;
}
let event = data[0];
let end = (data[1] & 0x80) != 0;
let volume = data[1] & 0x3F;
let duration = u16::from_be_bytes([data[2], data[3]]);
Some(DtmfEvent {
event,
end,
volume,
duration,
})
}
pub fn build_rtpmap_line(pt: u8) -> String {
format!("a=rtpmap:{} telephone-event/8000", pt)
}
pub fn build_fmtp_line(pt: u8) -> String {
format!("a=fmtp:{} 0-16", pt)
}
pub fn parse_telephone_event_pt(sdp: &str) -> Option<u8> {
for line in sdp.lines() {
let lower = line.to_lowercase().trim().to_string();
if lower.contains("telephone-event") {
if let Some(cap) = line
.split_whitespace()
.nth(0)
.and_then(|s| s.split(':').nth(1))
{
if let Ok(pt) = cap.parse::<u8>() {
return Some(pt);
}
}
}
}
None
}
pub fn sdp_has_telephone_event(sdp: &str) -> bool {
sdp.to_lowercase().contains("telephone-event")
}
pub fn inject_telephone_event_caps(sdp: &str, pt: u8) -> String {
let mut lines: Vec<String> = sdp.lines().map(|l| l.to_string()).collect();
if sdp_has_telephone_event(sdp) {
return sdp.to_string();
}
let mut mline_idx = None;
let mut last_rtpmap_idx = None;
for (i, line) in lines.iter().enumerate() {
let lower = line.to_lowercase();
if lower.starts_with("m=audio") {
mline_idx = Some(i);
}
if lower.starts_with("a=rtpmap:") {
last_rtpmap_idx = Some(i);
}
}
if let Some(mut idx) = mline_idx {
if let Some(rtpmap_end) = last_rtpmap_idx {
idx = rtpmap_end;
}
let mut new_pt = pt;
if let Some(mline) = mline_idx {
let parts: Vec<&str> = lines[mline].split_whitespace().collect();
if parts.len() >= 4 {
let audio_pts: Vec<&str> = parts[3..].to_vec();
while audio_pts.contains(&new_pt.to_string().as_str()) {
new_pt += 1;
}
}
}
let mline = &mut lines[mline_idx.unwrap()];
let payloads: Vec<&str> = mline.split_whitespace().collect();
let mut new_mline = payloads[..3].join(" ");
new_mline.push_str(&format!(" {}", new_pt));
for pt_str in &payloads[3..] {
new_mline.push_str(&format!(" {}", pt_str));
}
*mline = new_mline;
lines.insert(idx + 1, build_rtpmap_line(new_pt));
lines.insert(idx + 2, build_fmtp_line(new_pt));
}
lines.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_digit_event_roundtrip() {
for (i, &ch) in DTMF_DIGITS.iter().enumerate() {
assert_eq!(digit_to_event(ch), Some(i as u8));
assert_eq!(event_to_digit(i as u8), Some(ch));
}
}
#[test]
fn test_invalid_digit() {
assert_eq!(digit_to_event('X'), None);
assert_eq!(digit_to_event(' '), None);
}
#[test]
fn test_invalid_event() {
assert_eq!(event_to_digit(16), None);
assert_eq!(event_to_digit(255), None);
}
#[test]
fn test_encode_decode_roundtrip() {
let original = DtmfEvent {
event: 1,
end: false,
volume: 10,
duration: 160,
};
let encoded = encode_dtmf(original.clone());
assert_eq!(encoded.len(), 4);
let decoded = decode_dtmf(&encoded).unwrap();
assert_eq!(decoded.event, original.event);
assert_eq!(decoded.end, original.end);
assert_eq!(decoded.volume, original.volume);
assert_eq!(decoded.duration, original.duration);
}
#[test]
fn test_encode_decode_with_end_bit() {
let original = DtmfEvent {
event: 9,
end: true,
volume: 6,
duration: 480,
};
let encoded = encode_dtmf(original.clone());
let decoded = decode_dtmf(&encoded).unwrap();
assert_eq!(decoded.event, 9);
assert!(decoded.end);
assert_eq!(decoded.volume, 6);
assert_eq!(decoded.duration, 480);
}
#[test]
fn test_decode_short_buffer() {
assert!(decode_dtmf(&[0u8; 3]).is_none());
assert!(decode_dtmf(&[]).is_none());
}
#[test]
fn test_encode_all_digits() {
for digit in DTMF_DIGITS {
let ev = digit_to_event(*digit).unwrap();
let encoded = encode_dtmf(DtmfEvent {
event: ev,
end: true,
volume: 6,
duration: 800,
});
let decoded = decode_dtmf(&encoded).unwrap();
assert_eq!(event_to_digit(decoded.event), Some(*digit));
}
}
#[test]
fn test_rtpmap_line() {
let line = build_rtpmap_line(101);
assert_eq!(line, "a=rtpmap:101 telephone-event/8000");
}
#[test]
fn test_fmtp_line() {
let line = build_fmtp_line(101);
assert_eq!(line, "a=fmtp:101 0-16");
}
#[test]
fn test_parse_telephone_event_pt() {
let sdp = "a=rtpmap:101 telephone-event/8000\n";
assert_eq!(parse_telephone_event_pt(sdp), Some(101));
}
#[test]
fn test_parse_telephone_event_pt_no_match() {
let sdp = "a=rtpmap:0 PCMU/8000\n";
assert_eq!(parse_telephone_event_pt(sdp), None);
}
#[test]
fn test_sdp_has_telephone_event() {
assert!(sdp_has_telephone_event("telephone-event/8000"));
assert!(!sdp_has_telephone_event("PCMU/8000"));
}
#[test]
fn test_inject_telephone_event_caps_new() {
let sdp = "v=0\nm=audio 4000 RTP/AVP 0 8\na=rtpmap:0 PCMU/8000\na=rtpmap:8 PCMA/8000\n";
let result = inject_telephone_event_caps(sdp, 101);
assert!(result.contains("telephone-event"));
assert!(result.contains("101"));
assert!(result.contains("fmtp:101"));
}
#[test]
fn test_inject_telephone_event_caps_already_present() {
let sdp = "v=0\nm=audio 4000 RTP/AVP 0 101\na=rtpmap:0 PCMU/8000\na=rtpmap:101 telephone-event/8000\n";
let result = inject_telephone_event_caps(sdp, 101);
assert_eq!(result, sdp);
}
#[test]
fn test_multiple_packets_sequence() {
let events = vec![
DtmfEvent { event: 1, end: false, volume: 10, duration: 0 },
DtmfEvent { event: 1, end: false, volume: 10, duration: 160 },
DtmfEvent { event: 1, end: false, volume: 10, duration: 320 },
DtmfEvent { event: 1, end: true, volume: 10, duration: 480 },
];
for ev in &events {
let encoded = encode_dtmf(ev.clone());
let decoded = decode_dtmf(&encoded).unwrap();
assert_eq!(decoded.event, ev.event);
assert_eq!(decoded.duration, ev.duration);
assert_eq!(decoded.end, ev.end);
}
}
#[test]
fn test_volume_range() {
for vol in [0, 6, 20, 36] {
let ev = DtmfEvent {
event: 0,
end: true,
volume: vol,
duration: 160,
};
let encoded = encode_dtmf(ev.clone());
let decoded = decode_dtmf(&encoded).unwrap();
assert_eq!(decoded.volume, vol);
}
}
}