engine/sdp: CODECS override for the codecs we offer and accept

KPN's interconnect gateway transcodes every caller up: a G.711-only
fixed-line INVITE reached the phone offering PCMA, PCMU, AMR and AMR-WB
(bench call 2026-09-08), so the AMR-WB-only build rang and the narrowband
path — the one the Telia field report hit with 488 — cannot be reached on
the air through KPN by any caller.

CODECS=<list> (comma-separated over AMR-WB, AMR/AMR-NB, PCMA, PCMU) makes
the list both the codecs offered on an outgoing call and the acceptance
preference for an inbound offer, in that order: CODECS=PCMA takes G.711
A-law out of the mixed offer above, CODECS=PCMA,PCMU sends a G.711-only
offer toward the network. Unset, nothing changes — the default offer
bytes and the AMR-WB > AMR > PCMA > PCMU preference stay pinned. The
daemon logs an active override at startup.
This commit is contained in:
Jorijn van der Graaf 2026-09-08 17:53:26 +02:00
commit 37ec8660b7
6 changed files with 270 additions and 67 deletions

View file

@ -210,3 +210,4 @@ GPL-3.0-only — see [LICENSE](LICENSE).
Copyright (C) 2026 Catcrafts®
catcrafts.net
| `CODECS` | *(empty — defaults)* | comma-separated codec preference list over `AMR-WB`, `AMR` (or `AMR-NB`), `PCMA`, `PCMU`: restricts and orders both the codecs offered on an outgoing call and those accepted from an inbound offer (default: offer AMR-WB + AMR, accept all four in that order). A bench knob — a network whose gateway transcodes every caller up to AMR-WB otherwise never lets the narrowband path run |

View file

@ -571,6 +571,15 @@ public:
precond_ = EnvOr("PRECOND", "0") == "1";
ealgOffer_ = EnvOr("EALG", "aes-cbc");
mediaBin_ = ResolveMediaBin();
// CODECS: restrict + reorder the codecs we offer and accept (bench
// knob — a gateway that transcodes every caller up to AMR-WB never
// lets the narrowband path run otherwise). Empty = defaults.
codecs_ = imsd::sdp::ParseCodecList(EnvOr("CODECS", ""));
if (!codecs_.empty()) {
std::string list;
for (const auto& c : codecs_) list += (list.empty() ? "" : ",") + c;
Log(std::format("CODECS override active: {}", list));
}
// EMERGENCY_NUMBERS: comma-separated additions to the builtin
// 112/911 (a lab core's advertised short code, carrier extras).
std::string extra = EnvOr("EMERGENCY_NUMBERS", "");
@ -688,6 +697,7 @@ private:
int pcscfPort_ = 5060;
int rtpPort_ = 50004;
bool precond_ = false;
std::vector<std::string> codecs_; // CODECS override; empty = defaults
// registration
imsd::msg::Context ctx_;
@ -1138,7 +1148,7 @@ private:
Log(std::format("EMERGENCY dial ({}): urn:service:sos over the live registration, "
"fallback plain INVITE — stage 1, no emergency registration/PDN; "
"carrier-side behavior UNVERIFIED", c.number));
call_.emplace(ctx_, rng_, c.uni, c.number, rtpPort_, precond_, emergency);
call_.emplace(ctx_, rng_, c.uni, c.number, rtpPort_, precond_, emergency, codecs_);
CallInfo info{c.uni, c.number, "dialing", "outgoing", NowEpoch(), 0};
PostAdded(info);
bool ok = Execute(call_->Start());
@ -1198,7 +1208,7 @@ private:
}
std::string uni = std::format("ims-call-{}", ++callSeq_);
DumpRaw("imsd-invite-in.raw", msg);
call_.emplace(ctx_, rng_, uni, imsd::engine::IncomingInvite{msg}, rtpPort_);
call_.emplace(ctx_, rng_, uni, imsd::engine::IncomingInvite{msg}, rtpPort_, codecs_);
callReply_ = reply_;
Log(std::format("incoming call {} from {}", uni, call_->Number()));
// Validate the offer BEFORE the UI hears of the call. A refused

View file

@ -99,15 +99,20 @@ export namespace imsd::engine {
public:
// `emergency`: the shell classified the dialled string as an
// emergency number — the first attempt targets urn:service:sos.
CallMachine(const imsd::msg::Context& ctx, imsd::util::Rng& rng, std::string uni, std::string number, int rtpPort, bool precond, bool emergency = false)
// `codecs`: the daemon's CODECS override — the codecs offered on an
// outgoing call and accepted from an inbound offer, in preference
// order; empty = the sdp module's defaults.
CallMachine(const imsd::msg::Context& ctx, imsd::util::Rng& rng, std::string uni, std::string number, int rtpPort, bool precond, bool emergency = false,
std::vector<std::string> codecs = {})
: ctx_(ctx), rng_(rng), uni_(std::move(uni)), number_(std::move(number)),
rtpPort_(rtpPort), precond_(precond), attemptSos_(emergency) {
rtpPort_(rtpPort), precond_(precond), codecs_(std::move(codecs)), attemptSos_(emergency) {
ResetDialog();
}
CallMachine(const imsd::msg::Context& ctx, imsd::util::Rng& rng, std::string uni, IncomingInvite in, int rtpPort)
CallMachine(const imsd::msg::Context& ctx, imsd::util::Rng& rng, std::string uni, IncomingInvite in, int rtpPort,
std::vector<std::string> codecs = {})
: ctx_(ctx), rng_(rng), uni_(std::move(uni)), rtpPort_(rtpPort),
precond_(false), direction_(Direction::Incoming),
precond_(false), codecs_(std::move(codecs)), direction_(Direction::Incoming),
state_(CallState::Incoming), reason_("incoming"),
invite_(in.invite) {
number_ = imsd::sip::CallerId(invite_);
@ -148,6 +153,7 @@ export namespace imsd::engine {
offer.rtpPort = rtpPort_;
offer.sessionId = rng_.UInt(1000000, 9999999);
offer.precond = precond_;
offer.codecs = codecs_;
std::string sdp = imsd::sdp::BuildOffer(offer);
std::vector<Action> a;
a.push_back(Send(imsd::msg::BuildInvite(ctx_, d_, sdp, precond_)));
@ -158,8 +164,9 @@ export namespace imsd::engine {
// UAS: answer the inbound INVITE with 100 Trying + 180 Ringing (made
// reliable only when the caller Requires 100rel) and arm the ring
// timeout. An offer we cannot serve — no audio line, or none of the
// codecs imsd-media plays (AMR-WB, AMR, PCMA, PCMU) — is refused
// with 488 up front, before the UI ever rings; the log line names
// codecs imsd-media plays (AMR-WB, AMR, PCMA, PCMU; the CODECS
// override narrows that set) — is refused with 488 up front, before
// the UI ever rings; the log line names
// what WAS offered, so a field journal answers "which codec did the
// gateway want" without a raw SIP dump.
std::vector<Action> OnInvite() {
@ -318,6 +325,7 @@ export namespace imsd::engine {
offer.sessionId = rng_.UInt(1000000, 9999999);
offer.precond = precond_;
offer.currRemote = "sendrecv";
offer.codecs = codecs_;
ans = imsd::sdp::BuildOffer(offer);
}
// RFC 4028 §9: a 2xx to a refresh MUST echo Session-Expires,
@ -429,6 +437,7 @@ export namespace imsd::engine {
std::string uni_, number_;
int rtpPort_;
bool precond_;
std::vector<std::string> codecs_; // CODECS override; empty = defaults
Direction direction_ = Direction::Outgoing;
CallState state_ = CallState::Dialing;
std::string reason_ = "outgoing";
@ -532,7 +541,7 @@ export namespace imsd::engine {
}
MediaLeg ParseAnswer() const {
imsd::sdp::Answer ans = imsd::sdp::ParseAnswer(answerSdp_);
imsd::sdp::Answer ans = imsd::sdp::ParseAnswer(answerSdp_, codecs_);
MediaLeg leg;
leg.remoteIp = ans.ip;
leg.remotePort = ans.port < 0 ? 0 : ans.port;

View file

@ -5,15 +5,18 @@
/*
Imsd:Sdp — SDP offer/answer building + parsing for the voice media plane.
The offer is a fixed AMR-WB (octet-aligned) + AMR + telephone-event shape
with an optional GSMA IR.92 QoS-precondition block; it is what commercial IMS
cores expect from a VoLTE UE, and its exact bytes are pinned by tests/Sdp. The
parser extracts only what the media plane needs: remote address/port, the
negotiated payload type, the codec and octet-align mode — it reads answers
to our offers and inbound offers alike (same fields either way). The codecs
it knows are the ones imsd-media can play: AMR-WB, AMR (narrowband) and
G.711 (PCMA/PCMU) — the last two are what a PSTN gateway offers when a
landline calls. BuildAnswer is the terminating side: it answers an inbound
The offer is by default an AMR-WB (octet-aligned) + AMR + telephone-event
shape with an optional GSMA IR.92 QoS-precondition block; it is what
commercial IMS cores expect from a VoLTE UE, and its exact bytes are pinned by
tests/Sdp. The parser extracts only what the media plane needs: remote
address/port, the negotiated payload type, the codec and octet-align mode — it
reads answers to our offers and inbound offers alike (same fields either way).
The codecs it knows are the ones imsd-media can play: AMR-WB, AMR (narrowband)
and G.711 (PCMA/PCMU) — the last two are what a PSTN gateway offers when a
landline calls. An explicit codec list (the daemon's CODECS override) replaces
both the offered set and the acceptance preference: it is how the narrowband
path is exercised against a network whose gateway transcodes every caller up
to AMR-WB. BuildAnswer is the terminating side: it answers an inbound
offer with the offer's own payload type numbers (RFC 3264) and mirrors its
octet-align mode (RFC 4867 requires both directions to use one mode). Pure
std C++.
@ -49,20 +52,52 @@ namespace imsd::sdp {
export namespace imsd::sdp {
// The codec names imsd-media plays, in the default acceptance order:
// wideband beats narrowband, AMR beats G.711 (a mobile-to-mobile call
// must never land on a G.711 leg just because the gateway listed it).
constexpr std::array<std::string_view, 4> kCodecs{"AMR-WB", "AMR", "PCMA", "PCMU"};
// A codec preference list from its textual form ("PCMA, amr-nb"):
// case-insensitive, spaces ignored, AMR-NB accepted for AMR, unknown
// names and repeats dropped. Empty in = empty out = the defaults.
std::vector<std::string> ParseCodecList(std::string_view spec) {
std::vector<std::string> out;
for (auto part : std::views::split(spec, ',')) {
std::string n;
for (char c : std::string_view(part))
if (c != ' ' && c != '\t') n += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
if (n == "AMR-NB") n = "AMR";
if (std::ranges::find(kCodecs, n) == kCodecs.end()) continue;
if (std::ranges::find(out, n) == out.end()) out.push_back(n);
}
return out;
}
struct Offer {
std::string local; // connection address (v4 or v6)
int rtpPort = 0;
std::uint64_t sessionId = 0; // o= sess-id/version; caller randomizes
bool precond = false; // IR.92 QoS preconditions block
std::string currRemote = "none"; // a=curr:qos remote <this>
std::vector<std::string> codecs; // offered codecs in order; empty = AMR-WB, AMR
};
// Our offer: AMR-WB (octet-aligned) first, AMR narrowband second — the
// 3GPP-mandatory codec every IMS core and PSTN gateway can answer, so a
// call we place TO a landline no longer depends on the far side
// transcoding up to wideband — each with telephone-event at its own
// clock. Byte layout is pinned by tests/Sdp.
// clock. Byte layout is pinned by tests/Sdp. An explicit `codecs` list
// offers exactly those, in that order, with fixed payload types (97
// AMR-WB, 99 AMR, 8 PCMA, 0 PCMU) and telephone-event only at the clocks
// in use. G.711 is not offered by default: a VoLTE UE offering PCMA
// deviates from stock shapes for no gain.
std::string BuildOffer(const Offer& o) {
static const std::vector<std::string> kDefaultOffer{"AMR-WB", "AMR"};
const std::vector<std::string>& codecs = o.codecs.empty() ? kDefaultOffer : o.codecs;
bool wb = std::ranges::find(codecs, "AMR-WB") != codecs.end();
bool amr = std::ranges::find(codecs, "AMR") != codecs.end();
bool nb = codecs.size() > (wb ? 1u : 0u); // anything besides AMR-WB is 8 kHz
auto pt = [](const std::string& c) { return c == "AMR-WB" ? 97 : c == "AMR" ? 99 : c == "PCMA" ? 8 : 0; };
std::string ipver = Is6(o.local) ? "IP6" : "IP4";
std::string s;
s += "v=0\r\n";
@ -70,18 +105,33 @@ export namespace imsd::sdp {
s += "s=-\r\n";
s += std::format("c=IN {} {}\r\n", ipver, o.local);
s += "t=0 0\r\n";
s += std::format("m=audio {} RTP/AVP 97 99 98 100\r\n", o.rtpPort);
s += "b=AS:41\r\n";
std::string pts;
for (const auto& c : codecs) pts += std::format(" {}", pt(c));
if (wb) pts += " 98";
if (nb) pts += " 100";
s += std::format("m=audio {} RTP/AVP{}\r\n", o.rtpPort, pts);
s += std::format("b=AS:{}\r\n", wb ? 41 : amr ? 30 : 80);
s += "b=RS:512\r\n";
s += "b=RR:1536\r\n";
s += "a=rtpmap:97 AMR-WB/16000/1\r\n";
s += "a=fmtp:97 octet-align=1;mode-change-capability=2;max-red=0\r\n";
s += "a=rtpmap:99 AMR/8000/1\r\n";
s += "a=fmtp:99 octet-align=1;mode-change-capability=2;max-red=0\r\n";
s += "a=rtpmap:98 telephone-event/16000\r\n";
s += "a=fmtp:98 0-15\r\n";
s += "a=rtpmap:100 telephone-event/8000\r\n";
s += "a=fmtp:100 0-15\r\n";
for (const auto& c : codecs) {
if (c == "AMR-WB") {
s += "a=rtpmap:97 AMR-WB/16000/1\r\n";
s += "a=fmtp:97 octet-align=1;mode-change-capability=2;max-red=0\r\n";
} else if (c == "AMR") {
s += "a=rtpmap:99 AMR/8000/1\r\n";
s += "a=fmtp:99 octet-align=1;mode-change-capability=2;max-red=0\r\n";
} else {
s += std::format("a=rtpmap:{} {}/8000/1\r\n", pt(c), c);
}
}
if (wb) {
s += "a=rtpmap:98 telephone-event/16000\r\n";
s += "a=fmtp:98 0-15\r\n";
}
if (nb) {
s += "a=rtpmap:100 telephone-event/8000\r\n";
s += "a=fmtp:100 0-15\r\n";
}
s += "a=ptime:20\r\n";
s += "a=maxptime:240\r\n";
if (o.precond) {
@ -144,16 +194,16 @@ export namespace imsd::sdp {
}
// Payload-type preference: AMR-WB with octet-align=1 > AMR-WB > AMR with
// octet-align=1 > AMR > PCMA > PCMU. Octet-aligned wins within a codec
// because it is the media leg's native, call-proven format
// (bandwidth-efficient is supported but was the s56 static-audio
// octet-align=1 > AMR > PCMA > PCMU (kCodecs order). Octet-aligned wins
// within a codec because it is the media leg's native, call-proven
// format (bandwidth-efficient is supported but was the s56 static-audio
// culprit) — when the peer offers both variants, taking the
// octet-aligned pt keeps real calls on the battle-tested path. Wideband
// beats narrowband, AMR beats G.711 (a mobile-to-mobile call must never
// land on a G.711 leg just because the gateway listed it). With no
// playable codec at all, the first pt is used and codec comes back EMPTY
// — pretending otherwise would defeat codec checks upstream.
Answer ParseAnswer(std::string_view body) {
// octet-aligned pt keeps real calls on the battle-tested path. A
// non-empty `prefs` replaces the codec order (and restricts acceptance
// to the codecs named) — the CODECS override. With no acceptable codec
// at all, the first pt is used and codec comes back EMPTY — pretending
// otherwise would defeat codec checks upstream.
Answer ParseAnswer(std::string_view body, std::span<const std::string> prefs = {}) {
Answer a;
if (auto at = Find(body, "c=IN IP6 ")) a.ip = TokenAt(body, *at + 9);
else if (auto at4 = Find(body, "c=IN IP4 ")) a.ip = TokenAt(body, *at4 + 9);
@ -184,39 +234,29 @@ export namespace imsd::sdp {
std::string_view line = body.substr(*fm, lineEnd == std::string_view::npos ? std::string_view::npos : lineEnd - *fm);
return line.contains("octet-align=1");
};
int wbOa = -1;
int wb = -1;
int nbOa = -1;
int nb = -1;
int pcma = -1;
int pcmu = -1;
struct Seen { int first = -1; int oa = -1; };
std::map<std::string, Seen> seen; // codec name -> first pt, first octet-aligned pt
for (int p : pts) {
std::string name = CodecName(body, p);
if (name == "AMR-WB") {
if (wb == -1) wb = p;
if (wbOa == -1 && hasOctetAlign(p)) wbOa = p;
} else if (name == "AMR") {
if (nb == -1) nb = p;
if (nbOa == -1 && hasOctetAlign(p)) nbOa = p;
} else if (name == "PCMA") {
if (pcma == -1) pcma = p;
} else if (name == "PCMU") {
if (pcmu == -1) pcmu = p;
}
if (std::ranges::find(kCodecs, name) == kCodecs.end()) continue;
Seen& s = seen[name];
if (s.first == -1) s.first = p;
if (s.oa == -1 && name.starts_with("AMR") && hasOctetAlign(p)) s.oa = p;
}
if (wbOa != -1 || wb != -1) {
a.payloadType = wbOa != -1 ? wbOa : wb;
a.codec = "AMR-WB";
} else if (nbOa != -1 || nb != -1) {
a.payloadType = nbOa != -1 ? nbOa : nb;
a.codec = "AMR";
} else if (pcma != -1) {
a.payloadType = pcma;
a.codec = "PCMA";
} else if (pcmu != -1) {
a.payloadType = pcmu;
a.codec = "PCMU";
auto pick = [&](std::string_view name) {
auto it = seen.find(std::string(name));
if (it == seen.end()) return false;
a.payloadType = it->second.oa != -1 ? it->second.oa : it->second.first;
a.codec = std::string(name);
return true;
};
bool found = false;
if (prefs.empty()) {
for (std::string_view n : kCodecs) if ((found = pick(n))) break;
} else {
for (const std::string& n : prefs) if ((found = pick(n))) break;
}
if (!found) {
a.payloadType = pts.front();
a.codec.clear();
}

View file

@ -772,6 +772,62 @@ int main() {
Check(m.Terminated(), "abandoned machine is terminated");
}
// ---- CODECS override, terminating side: the transcoding gateway's
// mixed offer (KPN 2026-09-08) answered with G.711 A-law because the
// list says so — the on-air narrowband bench path
{
imsd::util::Rng rng(51);
std::string sdp =
"v=0\r\no=LucentPCSF 1 1 IN IP6 2001:db8::309\r\ns=-\r\n"
"c=IN IP6 2001:db8::309\r\nt=0 0\r\n"
"m=audio 41166 RTP/AVP 8 0 100 96 101 13 106\r\n"
"a=rtpmap:8 PCMA/8000\r\na=rtpmap:0 PCMU/8000\r\n"
"a=rtpmap:100 AMR/8000\r\na=fmtp:100 mode-change-period=2\r\n"
"a=rtpmap:96 AMR-WB/16000\r\na=fmtp:96 mode-change-period=2\r\n"
"a=rtpmap:101 telephone-event/8000\r\na=fmtp:101 0-15\r\n"
"a=rtpmap:106 telephone-event/16000\r\na=fmtp:106 0-15\r\n"
"a=sendrecv\r\na=ptime:20\r\na=maxptime:60\r\n";
CallMachine m(c, rng, "ims-call-20", IncomingInvite{MtInviteSdp("mt-kpn-pcma", sdp)}, 50004, {"PCMA"});
auto ai = m.OnInvite();
Check(Responded(ai, "SIP/2.0 180 Ringing") != nullptr, "CODECS=PCMA: mixed offer rings");
auto aa = m.OnAccept();
const Action* ok = Responded(aa, "SIP/2.0 200 OK");
Check(ok && ok->text.contains("m=audio 50004 RTP/AVP 8 101\r\n"), "CODECS=PCMA: answer is PCMA + narrowband DTMF at the offer's pts");
Check(ok && ok->text.contains("a=rtpmap:8 PCMA/8000/1\r\n") && !ok->text.contains("AMR"), "CODECS=PCMA: no AMR in the answer");
const Action* med = Find(aa, Action::Type::StartMedia);
Check(med && med->media.codec == "PCMA" && med->media.payloadType == 8 && !med->media.octetAlign, "CODECS=PCMA: media leg starts G.711 A-law");
// same offer, CODECS=AMR: narrowband AMR, bandwidth-efficient as offered
imsd::util::Rng rng2(52);
CallMachine m2(c, rng2, "ims-call-21", IncomingInvite{MtInviteSdp("mt-kpn-amr", sdp)}, 50004, {"AMR"});
m2.OnInvite();
auto aa2 = m2.OnAccept();
const Action* ok2 = Responded(aa2, "SIP/2.0 200 OK");
Check(ok2 && ok2->text.contains("m=audio 50004 RTP/AVP 100 101\r\n") && !ok2->text.contains("octet-align"), "CODECS=AMR: answer is AMR-NB BE + narrowband DTMF");
const Action* med2 = Find(aa2, Action::Type::StartMedia);
Check(med2 && med2->media.codec == "AMR" && med2->media.payloadType == 100 && !med2->media.octetAlign, "CODECS=AMR: media leg starts AMR-NB BE");
// same offer, no override: unchanged — AMR-WB
imsd::util::Rng rng3(53);
CallMachine m3(c, rng3, "ims-call-22", IncomingInvite{MtInviteSdp("mt-kpn-default", sdp)}, 50004);
m3.OnInvite();
auto aa3 = m3.OnAccept();
const Action* med3 = Find(aa3, Action::Type::StartMedia);
Check(med3 && med3->media.codec == "AMR-WB" && med3->media.payloadType == 96, "no override: the same offer still lands on AMR-WB");
}
// ---- CODECS override, originating side: the INVITE offers only what the
// list names (G.711 = a landline-shaped offer toward the network)
{
imsd::util::Rng rng(54);
CallMachine m(c, rng, "ims-call-23", "0783690354", 50004, false, false, {"PCMA", "PCMU"});
auto start = m.Start();
const Action* inv = Sent(start, "INVITE ");
Check(inv && inv->text.contains("m=audio 50004 RTP/AVP 8 0 100\r\n"), "CODECS=PCMA,PCMU: INVITE offers G.711 only");
Check(inv && !inv->text.contains("AMR"), "CODECS=PCMA,PCMU: no AMR in the offer");
Check(inv && inv->text.contains("a=rtpmap:100 telephone-event/8000\r\n") && !inv->text.contains("telephone-event/16000"), "CODECS=PCMA,PCMU: narrowband DTMF only");
}
if (Failures == 0) std::println("Engine: all tests passed");
return Failures;
}

View file

@ -294,6 +294,93 @@ int main() {
Check(sdp.contains("a=rtpmap:0 PCMU/8000/1\r\n") && !sdp.contains("fmtp:0"), "PCMU answer has no fmtp");
}
// ---- CODECS override: the offer of a real interconnect gateway that
// transcodes up (KPN, 2026-09-08: a G.711-only caller arrives with
// AMR-WB BE added). Default picks AMR-WB; a preference list picks
// what it names, in its order, and nothing outside it.
{
std::string kpn =
"c=IN IP6 2001:db8::309\r\n"
"m=audio 41166 RTP/AVP 8 0 100 96 101 13 106\r\n"
"a=rtpmap:8 PCMA/8000\r\na=rtpmap:0 PCMU/8000\r\n"
"a=rtpmap:100 AMR/8000\r\na=fmtp:100 mode-change-period=2\r\n"
"a=rtpmap:96 AMR-WB/16000\r\na=fmtp:96 mode-change-period=2\r\n"
"a=rtpmap:101 telephone-event/8000\r\na=fmtp:101 0-15\r\n"
"a=rtpmap:106 telephone-event/16000\r\na=fmtp:106 0-15\r\n"
"a=sendrecv\r\na=ptime:20\r\na=maxptime:60\r\n";
Answer d = ParseAnswer(kpn);
Check(d.payloadType == 96 && d.codec == "AMR-WB" && !d.octetAlign, "transcoding gateway: default takes AMR-WB BE");
std::vector<std::string> pcma{"PCMA"};
Answer a = ParseAnswer(kpn, pcma);
Check(a.payloadType == 8 && a.codec == "PCMA" && !a.octetAlign, "CODECS=PCMA takes G.711 A-law out of a mixed offer");
std::vector<std::string> ulawFirst{"PCMU", "PCMA"};
Answer u = ParseAnswer(kpn, ulawFirst);
Check(u.payloadType == 0 && u.codec == "PCMU", "CODECS order wins over the offer's order");
std::vector<std::string> nb{"AMR"};
Answer n = ParseAnswer(kpn, nb);
Check(n.payloadType == 100 && n.codec == "AMR" && !n.octetAlign, "CODECS=AMR takes narrowband AMR, BE as offered");
std::vector<std::string> wb{"AMR-WB"};
Answer w = ParseAnswer(kpn, wb);
Check(w.payloadType == 96 && w.codec == "AMR-WB", "CODECS=AMR-WB matches the default here");
Answer g = ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 8 0 101\r\n" "a=rtpmap:101 telephone-event/8000\r\n", wb);
Check(g.codec.empty() && g.payloadType == 8, "CODECS=AMR-WB refuses a G.711-only offer (codec empty, first pt)");
Check(g.port == 5000 && g.ip == "192.0.2.1", "address/port still parsed when no codec is acceptable");
}
// ---- ParseCodecList: case, spaces, AMR-NB alias, unknowns, repeats
{
auto l = ParseCodecList("pcma, amr-nb ,AMR-WB,g729,PCMA,,PCMU");
Check(l.size() == 4 && l[0] == "PCMA" && l[1] == "AMR" && l[2] == "AMR-WB" && l[3] == "PCMU", "codec list normalised: PCMA,AMR,AMR-WB,PCMU");
Check(ParseCodecList("").empty(), "empty spec = no override");
Check(ParseCodecList("opus, g722").empty(), "only unknown names = no override");
}
// ---- BuildOffer with an explicit codec list: G.711 only (landline
// shape, for MO tests against a transcoding network)
{
std::string sdp = BuildOffer({
.local = "192.0.2.1",
.rtpPort = 22222,
.sessionId = 1234567,
.codecs = {"PCMA", "PCMU"},
});
std::string expected =
"v=0\r\n"
"o=- 1234567 1234567 IN IP4 192.0.2.1\r\n"
"s=-\r\n"
"c=IN IP4 192.0.2.1\r\n"
"t=0 0\r\n"
"m=audio 22222 RTP/AVP 8 0 100\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:0 PCMU/8000/1\r\n"
"a=rtpmap:100 telephone-event/8000\r\n"
"a=fmtp:100 0-15\r\n"
"a=ptime:20\r\n"
"a=maxptime:240\r\n"
"a=sendrecv\r\n";
Check(sdp == expected, "G.711-only offer byte-exact");
}
// ---- BuildOffer, AMR narrowband only: pt 99, narrowband DTMF only, AS 30
{
std::string sdp = BuildOffer({.local = "2001:db8::1", .rtpPort = 50004, .sessionId = 7, .codecs = {"AMR"}});
Check(sdp.contains("m=audio 50004 RTP/AVP 99 100\r\n"), "AMR-only offer m-line");
Check(sdp.contains("b=AS:30\r\n"), "AMR-only offer bandwidth");
Check(sdp.contains("a=rtpmap:99 AMR/8000/1\r\na=fmtp:99 octet-align=1;mode-change-capability=2;max-red=0\r\n"), "AMR-only offer rtpmap+fmtp");
Check(!sdp.contains("16000"), "no wideband telephone-event without AMR-WB");
}
// ---- BuildOffer, explicit default order == the pinned default bytes
{
Offer o{.local = "2001:db8::1", .rtpPort = 50004, .sessionId = 7};
Offer e = o;
e.codecs = {"AMR-WB", "AMR"};
Check(BuildOffer(o) == BuildOffer(e), "explicit AMR-WB,AMR list is byte-identical to the default offer");
}
if (Failures == 0) std::println("Sdp: all tests passed");
return Failures;
}