engine/sdp: accept AMR-NB and G.711 offers; name the offered codecs on 488

An incoming INVITE whose offer carried no AMR-WB was refused with 488
before ringing. A landline caller arrives through the PSTN gateway, which
offers narrowband — AMR (NB) and/or G.711 — so every landline call was
refused, deterministically. Field report 2026-09-01 (Telia Norge): a
doctor's office called twice, both 488; a mobile caller rang fine.

The parser now recognises PCMA/PCMU by rtpmap name or by static payload
type (a G.711 offer may carry no rtpmap line at all, RFC 3551 §6), matches
encoding names case-insensitively (RFC 4566 §6), and prefers
AMR-WB > AMR > PCMA > PCMU with octet-aligned first within AMR — a mobile
caller offering everything still lands on AMR-WB. The engine accepts any
codec the media leg plays and answers at the offer's own payload type with
the DTMF clock matching the codec (8 kHz for narrowband). A refused offer
now logs what WAS offered ('offered: 18 G729, 101 TELEPHONE-EVENT'), so a
field journal answers 'which codec did the gateway want' without a raw SIP
dump.

The media leg gains the matching codecs in the next commit.
This commit is contained in:
Jorijn van der Graaf 2026-09-02 03:11:11 +02:00
commit daaba2d657
4 changed files with 295 additions and 57 deletions

View file

@ -126,12 +126,68 @@ int main() {
Check(a.payloadType == 96 && a.codec == "AMR", "plain AMR fallback");
Check(a.octetAlign, "octet-align seen for AMR too");
}
// ---- no rtpmap at all -> first payload type, bandwidth-efficient
// ---- G.711 by static payload type, no rtpmap at all (RFC 3551 §6):
// the shape a PSTN gateway sends for a landline caller
{
Answer a = ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 8 0\r\n");
Check(a.payloadType == 8, "no rtpmap -> first payload type");
Answer a = ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 0 8\r\n");
Check(a.payloadType == 8, "PCMA preferred over an earlier PCMU");
Check(a.codec == "PCMA", "static pt 8 -> PCMA without an rtpmap line");
Check(!a.octetAlign, "G.711 never octet-aligned");
}
{
Answer a = ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 0\r\n");
Check(a.payloadType == 0 && a.codec == "PCMU", "static pt 0 -> PCMU");
}
// ---- G.711 with an rtpmap, lower-case name (RFC 4566: case-insensitive)
{
Answer a = ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 8 101\r\n" "a=rtpmap:8 pcma/8000\r\n" "a=rtpmap:101 telephone-event/8000\r\n");
Check(a.payloadType == 8 && a.codec == "PCMA", "lower-case rtpmap name still recognised");
}
{
Answer a = ParseAnswer("c=IN IP6 2001:db8::1\r\n" "m=audio 5000 RTP/AVP 96\r\n" "a=rtpmap:96 amr-wb/16000/1\r\n" "a=fmtp:96 octet-align=1\r\n");
Check(a.payloadType == 96 && a.codec == "AMR-WB" && a.octetAlign, "lower-case amr-wb recognised, octet-align kept");
}
// ---- narrowband AMR beats G.711 (a gateway listing both)
{
Answer a = ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 8 100 101\r\n" "a=rtpmap:100 AMR/8000/1\r\n" "a=fmtp:100 octet-align=1\r\n" "a=rtpmap:101 telephone-event/8000\r\n");
Check(a.payloadType == 100 && a.codec == "AMR", "AMR preferred over PCMA listed first");
Check(a.octetAlign, "AMR octet-align seen");
}
// ---- wideband beats everything: KPN's real mobile-origin list
// (AMR-WB BE 116, AMR-WB OA 107, PCMA 8, AMR 100, DTMF 111/110, CN 13)
{
Answer a = ParseAnswer(
"c=IN IP6 2001:db8::310\r\n"
"m=audio 5000 RTP/AVP 116 107 8 100 111 110 13\r\n"
"a=rtpmap:116 AMR-WB/16000/1\r\n"
"a=fmtp:116 mode-change-capability=2;max-red=0\r\n"
"a=rtpmap:107 AMR-WB/16000/1\r\n"
"a=fmtp:107 octet-align=1;mode-change-capability=2;max-red=0\r\n"
"a=rtpmap:100 AMR/8000/1\r\n"
"a=rtpmap:111 telephone-event/16000\r\n"
"a=rtpmap:110 telephone-event/8000\r\n"
"a=rtpmap:13 CN/8000\r\n");
Check(a.payloadType == 107 && a.codec == "AMR-WB" && a.octetAlign, "mobile caller still lands on octet-aligned AMR-WB");
}
// ---- nothing we play -> first payload type, codec EMPTY (the 488 case)
{
Answer a = ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 18 101\r\n" "a=rtpmap:18 G729/8000\r\n" "a=rtpmap:101 telephone-event/8000\r\n");
Check(a.payloadType == 18, "no playable codec -> first payload type");
Check(!a.octetAlign, "no fmtp -> bandwidth-efficient");
Check(a.codec.empty(), "no AMR rtpmap -> codec unknown, not assumed");
Check(a.codec.empty(), "G729-only -> codec unknown, not assumed");
}
// ---- OfferedCodecs: the 488 log summary
{
Check(OfferedCodecs("m=audio 5000 RTP/AVP 18 101\r\na=rtpmap:18 G729/8000\r\na=rtpmap:101 telephone-event/8000\r\n") == "18 G729, 101 TELEPHONE-EVENT", "offered list names each pt");
Check(OfferedCodecs("m=audio 5000 RTP/AVP 0 8\r\n") == "0 PCMU, 8 PCMA", "static G.711 types named without rtpmap");
Check(OfferedCodecs("m=audio 5000 RTP/AVP 96\r\n") == "96", "unknown dynamic pt listed bare");
Check(OfferedCodecs("v=0\r\n") == "no m=audio line", "no audio line says so");
}
// ---- CodecRate
{
Check(CodecRate("AMR-WB") == 16000, "AMR-WB is 16 kHz");
Check(CodecRate("AMR") == 8000 && CodecRate("PCMA") == 8000 && CodecRate("PCMU") == 8000, "narrowband codecs are 8 kHz");
}
// ---- degenerate body
{
@ -198,6 +254,41 @@ int main() {
Check(!sdp.contains("telephone-event"), "no DTMF when the offer had none");
Check(sdp.contains("b=AS:30\r\n"), "narrowband bandwidth");
}
// ---- BuildAnswer: G.711 for a landline caller — static pt, no fmtp,
// narrowband DTMF, byte-exact
{
std::string sdp = BuildAnswer({
.local = "2001:db8:29e9:a05f::1",
.rtpPort = 50004,
.sessionId = 778,
.payloadType = 8,
.octetAlign = false,
.codec = "PCMA",
.dtmfPt = 101,
});
std::string expected =
"v=0\r\n"
"o=- 778 778 IN IP6 2001:db8:29e9:a05f::1\r\n"
"s=-\r\n"
"c=IN IP6 2001:db8:29e9:a05f::1\r\n"
"t=0 0\r\n"
"m=audio 50004 RTP/AVP 8 101\r\n"
"b=AS:80\r\n"
"b=RS:512\r\n"
"b=RR:1536\r\n"
"a=rtpmap:8 PCMA/8000/1\r\n"
"a=rtpmap:101 telephone-event/8000\r\n"
"a=fmtp:101 0-15\r\n"
"a=ptime:20\r\n"
"a=maxptime:240\r\n"
"a=sendrecv\r\n";
Check(sdp == expected, "PCMA answer is byte-exact");
}
// ---- BuildAnswer never marks G.711 octet-aligned even if asked
{
std::string sdp = BuildAnswer({.local = "10.0.0.2", .rtpPort = 4000, .sessionId = 9, .payloadType = 0, .octetAlign = true, .codec = "PCMU"});
Check(sdp.contains("a=rtpmap:0 PCMU/8000/1\r\n") && !sdp.contains("fmtp:0"), "PCMU answer has no fmtp");
}
if (Failures == 0) std::println("Sdp: all tests passed");
return Failures;