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:
parent
452634683c
commit
37ec8660b7
6 changed files with 270 additions and 67 deletions
|
|
@ -210,3 +210,4 @@ GPL-3.0-only — see [LICENSE](LICENSE).
|
||||||
|
|
||||||
Copyright (C) 2026 Catcrafts®
|
Copyright (C) 2026 Catcrafts®
|
||||||
catcrafts.net
|
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 |
|
||||||
|
|
|
||||||
|
|
@ -571,6 +571,15 @@ public:
|
||||||
precond_ = EnvOr("PRECOND", "0") == "1";
|
precond_ = EnvOr("PRECOND", "0") == "1";
|
||||||
ealgOffer_ = EnvOr("EALG", "aes-cbc");
|
ealgOffer_ = EnvOr("EALG", "aes-cbc");
|
||||||
mediaBin_ = ResolveMediaBin();
|
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
|
// EMERGENCY_NUMBERS: comma-separated additions to the builtin
|
||||||
// 112/911 (a lab core's advertised short code, carrier extras).
|
// 112/911 (a lab core's advertised short code, carrier extras).
|
||||||
std::string extra = EnvOr("EMERGENCY_NUMBERS", "");
|
std::string extra = EnvOr("EMERGENCY_NUMBERS", "");
|
||||||
|
|
@ -688,6 +697,7 @@ private:
|
||||||
int pcscfPort_ = 5060;
|
int pcscfPort_ = 5060;
|
||||||
int rtpPort_ = 50004;
|
int rtpPort_ = 50004;
|
||||||
bool precond_ = false;
|
bool precond_ = false;
|
||||||
|
std::vector<std::string> codecs_; // CODECS override; empty = defaults
|
||||||
|
|
||||||
// registration
|
// registration
|
||||||
imsd::msg::Context ctx_;
|
imsd::msg::Context ctx_;
|
||||||
|
|
@ -1138,7 +1148,7 @@ private:
|
||||||
Log(std::format("EMERGENCY dial ({}): urn:service:sos over the live registration, "
|
Log(std::format("EMERGENCY dial ({}): urn:service:sos over the live registration, "
|
||||||
"fallback plain INVITE — stage 1, no emergency registration/PDN; "
|
"fallback plain INVITE — stage 1, no emergency registration/PDN; "
|
||||||
"carrier-side behavior UNVERIFIED", c.number));
|
"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};
|
CallInfo info{c.uni, c.number, "dialing", "outgoing", NowEpoch(), 0};
|
||||||
PostAdded(info);
|
PostAdded(info);
|
||||||
bool ok = Execute(call_->Start());
|
bool ok = Execute(call_->Start());
|
||||||
|
|
@ -1198,7 +1208,7 @@ private:
|
||||||
}
|
}
|
||||||
std::string uni = std::format("ims-call-{}", ++callSeq_);
|
std::string uni = std::format("ims-call-{}", ++callSeq_);
|
||||||
DumpRaw("imsd-invite-in.raw", msg);
|
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_;
|
callReply_ = reply_;
|
||||||
Log(std::format("incoming call {} from {}", uni, call_->Number()));
|
Log(std::format("incoming call {} from {}", uni, call_->Number()));
|
||||||
// Validate the offer BEFORE the UI hears of the call. A refused
|
// Validate the offer BEFORE the UI hears of the call. A refused
|
||||||
|
|
|
||||||
|
|
@ -99,15 +99,20 @@ export namespace imsd::engine {
|
||||||
public:
|
public:
|
||||||
// `emergency`: the shell classified the dialled string as an
|
// `emergency`: the shell classified the dialled string as an
|
||||||
// emergency number — the first attempt targets urn:service:sos.
|
// 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)),
|
: 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();
|
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),
|
: 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"),
|
state_(CallState::Incoming), reason_("incoming"),
|
||||||
invite_(in.invite) {
|
invite_(in.invite) {
|
||||||
number_ = imsd::sip::CallerId(invite_);
|
number_ = imsd::sip::CallerId(invite_);
|
||||||
|
|
@ -148,6 +153,7 @@ export namespace imsd::engine {
|
||||||
offer.rtpPort = rtpPort_;
|
offer.rtpPort = rtpPort_;
|
||||||
offer.sessionId = rng_.UInt(1000000, 9999999);
|
offer.sessionId = rng_.UInt(1000000, 9999999);
|
||||||
offer.precond = precond_;
|
offer.precond = precond_;
|
||||||
|
offer.codecs = codecs_;
|
||||||
std::string sdp = imsd::sdp::BuildOffer(offer);
|
std::string sdp = imsd::sdp::BuildOffer(offer);
|
||||||
std::vector<Action> a;
|
std::vector<Action> a;
|
||||||
a.push_back(Send(imsd::msg::BuildInvite(ctx_, d_, sdp, precond_)));
|
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
|
// UAS: answer the inbound INVITE with 100 Trying + 180 Ringing (made
|
||||||
// reliable only when the caller Requires 100rel) and arm the ring
|
// reliable only when the caller Requires 100rel) and arm the ring
|
||||||
// timeout. An offer we cannot serve — no audio line, or none of the
|
// timeout. An offer we cannot serve — no audio line, or none of the
|
||||||
// codecs imsd-media plays (AMR-WB, AMR, PCMA, PCMU) — is refused
|
// codecs imsd-media plays (AMR-WB, AMR, PCMA, PCMU; the CODECS
|
||||||
// with 488 up front, before the UI ever rings; the log line names
|
// 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
|
// what WAS offered, so a field journal answers "which codec did the
|
||||||
// gateway want" without a raw SIP dump.
|
// gateway want" without a raw SIP dump.
|
||||||
std::vector<Action> OnInvite() {
|
std::vector<Action> OnInvite() {
|
||||||
|
|
@ -318,6 +325,7 @@ export namespace imsd::engine {
|
||||||
offer.sessionId = rng_.UInt(1000000, 9999999);
|
offer.sessionId = rng_.UInt(1000000, 9999999);
|
||||||
offer.precond = precond_;
|
offer.precond = precond_;
|
||||||
offer.currRemote = "sendrecv";
|
offer.currRemote = "sendrecv";
|
||||||
|
offer.codecs = codecs_;
|
||||||
ans = imsd::sdp::BuildOffer(offer);
|
ans = imsd::sdp::BuildOffer(offer);
|
||||||
}
|
}
|
||||||
// RFC 4028 §9: a 2xx to a refresh MUST echo Session-Expires,
|
// RFC 4028 §9: a 2xx to a refresh MUST echo Session-Expires,
|
||||||
|
|
@ -429,6 +437,7 @@ export namespace imsd::engine {
|
||||||
std::string uni_, number_;
|
std::string uni_, number_;
|
||||||
int rtpPort_;
|
int rtpPort_;
|
||||||
bool precond_;
|
bool precond_;
|
||||||
|
std::vector<std::string> codecs_; // CODECS override; empty = defaults
|
||||||
Direction direction_ = Direction::Outgoing;
|
Direction direction_ = Direction::Outgoing;
|
||||||
CallState state_ = CallState::Dialing;
|
CallState state_ = CallState::Dialing;
|
||||||
std::string reason_ = "outgoing";
|
std::string reason_ = "outgoing";
|
||||||
|
|
@ -532,7 +541,7 @@ export namespace imsd::engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaLeg ParseAnswer() const {
|
MediaLeg ParseAnswer() const {
|
||||||
imsd::sdp::Answer ans = imsd::sdp::ParseAnswer(answerSdp_);
|
imsd::sdp::Answer ans = imsd::sdp::ParseAnswer(answerSdp_, codecs_);
|
||||||
MediaLeg leg;
|
MediaLeg leg;
|
||||||
leg.remoteIp = ans.ip;
|
leg.remoteIp = ans.ip;
|
||||||
leg.remotePort = ans.port < 0 ? 0 : ans.port;
|
leg.remotePort = ans.port < 0 ? 0 : ans.port;
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,18 @@
|
||||||
/*
|
/*
|
||||||
Imsd:Sdp — SDP offer/answer building + parsing for the voice media plane.
|
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
|
The offer is by default an AMR-WB (octet-aligned) + AMR + telephone-event
|
||||||
with an optional GSMA IR.92 QoS-precondition block; it is what commercial IMS
|
shape with an optional GSMA IR.92 QoS-precondition block; it is what
|
||||||
cores expect from a VoLTE UE, and its exact bytes are pinned by tests/Sdp. The
|
commercial IMS cores expect from a VoLTE UE, and its exact bytes are pinned by
|
||||||
parser extracts only what the media plane needs: remote address/port, the
|
tests/Sdp. The parser extracts only what the media plane needs: remote
|
||||||
negotiated payload type, the codec and octet-align mode — it reads answers
|
address/port, the negotiated payload type, the codec and octet-align mode — it
|
||||||
to our offers and inbound offers alike (same fields either way). The codecs
|
reads answers to our offers and inbound offers alike (same fields either way).
|
||||||
it knows are the ones imsd-media can play: AMR-WB, AMR (narrowband) and
|
The codecs it knows are the ones imsd-media can play: AMR-WB, AMR (narrowband)
|
||||||
G.711 (PCMA/PCMU) — the last two are what a PSTN gateway offers when a
|
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
|
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
|
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
|
octet-align mode (RFC 4867 requires both directions to use one mode). Pure
|
||||||
std C++.
|
std C++.
|
||||||
|
|
@ -49,20 +52,52 @@ namespace imsd::sdp {
|
||||||
|
|
||||||
export 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 {
|
struct Offer {
|
||||||
std::string local; // connection address (v4 or v6)
|
std::string local; // connection address (v4 or v6)
|
||||||
int rtpPort = 0;
|
int rtpPort = 0;
|
||||||
std::uint64_t sessionId = 0; // o= sess-id/version; caller randomizes
|
std::uint64_t sessionId = 0; // o= sess-id/version; caller randomizes
|
||||||
bool precond = false; // IR.92 QoS preconditions block
|
bool precond = false; // IR.92 QoS preconditions block
|
||||||
std::string currRemote = "none"; // a=curr:qos remote <this>
|
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
|
// Our offer: AMR-WB (octet-aligned) first, AMR narrowband second — the
|
||||||
// 3GPP-mandatory codec every IMS core and PSTN gateway can answer, so a
|
// 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
|
// call we place TO a landline no longer depends on the far side
|
||||||
// transcoding up to wideband — each with telephone-event at its own
|
// 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) {
|
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 ipver = Is6(o.local) ? "IP6" : "IP4";
|
||||||
std::string s;
|
std::string s;
|
||||||
s += "v=0\r\n";
|
s += "v=0\r\n";
|
||||||
|
|
@ -70,18 +105,33 @@ export namespace imsd::sdp {
|
||||||
s += "s=-\r\n";
|
s += "s=-\r\n";
|
||||||
s += std::format("c=IN {} {}\r\n", ipver, o.local);
|
s += std::format("c=IN {} {}\r\n", ipver, o.local);
|
||||||
s += "t=0 0\r\n";
|
s += "t=0 0\r\n";
|
||||||
s += std::format("m=audio {} RTP/AVP 97 99 98 100\r\n", o.rtpPort);
|
std::string pts;
|
||||||
s += "b=AS:41\r\n";
|
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=RS:512\r\n";
|
||||||
s += "b=RR:1536\r\n";
|
s += "b=RR:1536\r\n";
|
||||||
|
for (const auto& c : codecs) {
|
||||||
|
if (c == "AMR-WB") {
|
||||||
s += "a=rtpmap:97 AMR-WB/16000/1\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=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=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=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=rtpmap:98 telephone-event/16000\r\n";
|
||||||
s += "a=fmtp:98 0-15\r\n";
|
s += "a=fmtp:98 0-15\r\n";
|
||||||
|
}
|
||||||
|
if (nb) {
|
||||||
s += "a=rtpmap:100 telephone-event/8000\r\n";
|
s += "a=rtpmap:100 telephone-event/8000\r\n";
|
||||||
s += "a=fmtp:100 0-15\r\n";
|
s += "a=fmtp:100 0-15\r\n";
|
||||||
|
}
|
||||||
s += "a=ptime:20\r\n";
|
s += "a=ptime:20\r\n";
|
||||||
s += "a=maxptime:240\r\n";
|
s += "a=maxptime:240\r\n";
|
||||||
if (o.precond) {
|
if (o.precond) {
|
||||||
|
|
@ -144,16 +194,16 @@ export namespace imsd::sdp {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payload-type preference: AMR-WB with octet-align=1 > AMR-WB > AMR with
|
// 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
|
// octet-align=1 > AMR > PCMA > PCMU (kCodecs order). Octet-aligned wins
|
||||||
// because it is the media leg's native, call-proven format
|
// within a codec because it is the media leg's native, call-proven
|
||||||
// (bandwidth-efficient is supported but was the s56 static-audio
|
// format (bandwidth-efficient is supported but was the s56 static-audio
|
||||||
// culprit) — when the peer offers both variants, taking the
|
// culprit) — when the peer offers both variants, taking the
|
||||||
// octet-aligned pt keeps real calls on the battle-tested path. Wideband
|
// octet-aligned pt keeps real calls on the battle-tested path. A
|
||||||
// beats narrowband, AMR beats G.711 (a mobile-to-mobile call must never
|
// non-empty `prefs` replaces the codec order (and restricts acceptance
|
||||||
// land on a G.711 leg just because the gateway listed it). With no
|
// to the codecs named) — the CODECS override. With no acceptable codec
|
||||||
// playable codec at all, the first pt is used and codec comes back EMPTY
|
// at all, the first pt is used and codec comes back EMPTY — pretending
|
||||||
// — pretending otherwise would defeat codec checks upstream.
|
// otherwise would defeat codec checks upstream.
|
||||||
Answer ParseAnswer(std::string_view body) {
|
Answer ParseAnswer(std::string_view body, std::span<const std::string> prefs = {}) {
|
||||||
Answer a;
|
Answer a;
|
||||||
if (auto at = Find(body, "c=IN IP6 ")) a.ip = TokenAt(body, *at + 9);
|
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);
|
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);
|
std::string_view line = body.substr(*fm, lineEnd == std::string_view::npos ? std::string_view::npos : lineEnd - *fm);
|
||||||
return line.contains("octet-align=1");
|
return line.contains("octet-align=1");
|
||||||
};
|
};
|
||||||
int wbOa = -1;
|
struct Seen { int first = -1; int oa = -1; };
|
||||||
int wb = -1;
|
std::map<std::string, Seen> seen; // codec name -> first pt, first octet-aligned pt
|
||||||
int nbOa = -1;
|
|
||||||
int nb = -1;
|
|
||||||
int pcma = -1;
|
|
||||||
int pcmu = -1;
|
|
||||||
for (int p : pts) {
|
for (int p : pts) {
|
||||||
std::string name = CodecName(body, p);
|
std::string name = CodecName(body, p);
|
||||||
if (name == "AMR-WB") {
|
if (std::ranges::find(kCodecs, name) == kCodecs.end()) continue;
|
||||||
if (wb == -1) wb = p;
|
Seen& s = seen[name];
|
||||||
if (wbOa == -1 && hasOctetAlign(p)) wbOa = p;
|
if (s.first == -1) s.first = p;
|
||||||
} else if (name == "AMR") {
|
if (s.oa == -1 && name.starts_with("AMR") && hasOctetAlign(p)) s.oa = p;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
auto pick = [&](std::string_view name) {
|
||||||
if (wbOa != -1 || wb != -1) {
|
auto it = seen.find(std::string(name));
|
||||||
a.payloadType = wbOa != -1 ? wbOa : wb;
|
if (it == seen.end()) return false;
|
||||||
a.codec = "AMR-WB";
|
a.payloadType = it->second.oa != -1 ? it->second.oa : it->second.first;
|
||||||
} else if (nbOa != -1 || nb != -1) {
|
a.codec = std::string(name);
|
||||||
a.payloadType = nbOa != -1 ? nbOa : nb;
|
return true;
|
||||||
a.codec = "AMR";
|
};
|
||||||
} else if (pcma != -1) {
|
bool found = false;
|
||||||
a.payloadType = pcma;
|
if (prefs.empty()) {
|
||||||
a.codec = "PCMA";
|
for (std::string_view n : kCodecs) if ((found = pick(n))) break;
|
||||||
} else if (pcmu != -1) {
|
|
||||||
a.payloadType = pcmu;
|
|
||||||
a.codec = "PCMU";
|
|
||||||
} else {
|
} else {
|
||||||
|
for (const std::string& n : prefs) if ((found = pick(n))) break;
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
a.payloadType = pts.front();
|
a.payloadType = pts.front();
|
||||||
a.codec.clear();
|
a.codec.clear();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -772,6 +772,62 @@ int main() {
|
||||||
Check(m.Terminated(), "abandoned machine is terminated");
|
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");
|
if (Failures == 0) std::println("Engine: all tests passed");
|
||||||
return Failures;
|
return Failures;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
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");
|
if (Failures == 0) std::println("Sdp: all tests passed");
|
||||||
return Failures;
|
return Failures;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue