sdp, engine, imsd: SendDtmf sends the tones as RFC 4733 events over the media leg

SendDtmf was a stub: GNOME Calls' SendTones and Plasma Dialer's sendDtmf
reached imsd and went nowhere. The SDP parser reads the telephone-event
payload type at the chosen codec's clock into the answer, among the
m-line's formats only — an rtpmap the line does not list is not
negotiated, and nothing is sent with a format the answer does not carry
(RFC 3264 §8.3.2); an event's rate is its rtpmap clock (RFC 4733
§2.4.1), an offer with two codec clocks lists telephone-event at each,
and the name is matched as every encoding name is, case-insensitively,
with the clock whole. The media leg carries it, and imsd-media gets it
as DTMF_PT with its stdin on a pipe from imsd, the control pipe. The
SDP answer imsd sends to an inbound offer carries telephone-event by the
same rule, where it echoed any telephone-event rtpmap with a lower-case
name and a clock prefix before: an rtpmap off the offer's m-line is no
longer echoed, an upper-case name now is. The decision is the engine's:
OnDtmf lets the tones through on an active call that negotiated
telephone-event (the answer's number on a call we placed, the offer's on
one we answered) and drops them otherwise with a line that says why:
before the answer, after the call, no telephone-event at the codec's
clock in the SDP, a character that is no tone or more than 128 tones
(then the whole string, never part of what was pressed); letters are
upper-cased. The shell writes one `dtmf <tones>` line per SendDtmf,
whole or not at all: the bound keeps the line within one atomic pipe
write, the write end is non-blocking so that a leg that stopped reading
fails the write instead of holding the engine thread, and a longer line
is refused; SIGPIPE is ignored so that a write after the leg died fails
with EPIPE instead of ending the daemon. The command queue's one
argument is `arg` now — the number to dial, or the tones. The `media …`
start line gains the telephone-event pt at its end.

Tested: tests/Sdp and tests/Engine (the eight suites pass); the dev
phone's dtmf benches ran in builds of this series
(journal/ims/captures/2026-10-14-dtmf-bench-1/ onward).
This commit is contained in:
Jorijn van der Graaf 2026-10-14 15:05:24 +02:00
commit 92fba40da0
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
6 changed files with 365 additions and 73 deletions

View file

@ -207,7 +207,21 @@ int main() {
"a=rtpmap:105 telephone-event/16000\r\n"
"a=rtpmap:106 telephone-event/8000\r\n";
Check(TelephoneEventPt(offer, 16000) == 105, "telephone-event pt by clock rate");
Check(TelephoneEventPt(offer, 8000) == 106, "narrowband telephone-event pt");
Check(!TelephoneEventPt(offer, 8000), "an rtpmap the m-line does not list (106 among 104 105) is not negotiated");
Check(ParseAnswer(offer).dtmfPt == 105, "the answer's dtmfPt is the one at the chosen codec's clock (AMR-WB: 16000)");
std::string both = std::format("{}a=rtpmap:99 AMR/8000/1\r\n", offer);
both.replace(both.find("RTP/AVP 104 105"), 15, "RTP/AVP 104 99 105 106");
std::array<std::string, 1> nb = {"AMR"};
Check(ParseAnswer(both, nb).dtmfPt == 106, "...and the narrowband one when AMR is chosen");
Check(TelephoneEventPt(both, 8000) == 106, "listed on the m-line, the narrowband telephone-event pt is found");
Check(!ParseAnswer(offer, nb).dtmfPt, "no codec we play chosen: no dtmfPt either");
Check(!ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 8\r\n" "a=rtpmap:8 PCMA/8000\r\n").dtmfPt, "no telephone-event in the answer: dtmfPt empty");
Check(!ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 8\r\n" "a=rtpmap:8 PCMA/8000\r\n" "a=rtpmap:101 telephone-event/8000\r\n").dtmfPt, "the answer dropped telephone-event from its m-line and kept the rtpmap: not negotiated (RFC 3264 §8.3.2: nothing is sent with a format the answer does not list)");
Check(ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 8 101\r\n" "a=rtpmap:101 telephone-event/8000\r\n").dtmfPt == 101, "G.711 answer with telephone-event/8000: dtmfPt 101");
Check(!ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 8 101\r\n" "a=rtpmap:101 telephone-event/16000\r\n").dtmfPt, "telephone-event at the wrong clock for the codec is not the call's");
Check(ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 8 101\r\n" "a=rtpmap:101 TELEPHONE-EVENT/8000\r\n").dtmfPt == 101, "the name in another case (RFC 4566: case-insensitive)");
Check(TelephoneEventPt("m=audio 1 RTP/AVP 98\r\na=rtpmap:98 Telephone-Event/16000/1\r\n", 16000) == 98, "mixed case, with a channel count");
Check(!TelephoneEventPt("m=audio 1 RTP/AVP 98\r\na=rtpmap:98 telephone-event/80000\r\n", 8000), "the clock is matched whole: /80000 is not /8000");
Check(!TelephoneEventPt(offer, 48000).has_value(), "absent clock rate -> nullopt");
Check(!TelephoneEventPt("m=audio 1 RTP/AVP 0\r\n", 16000).has_value(), "no rtpmap -> nullopt");
}