Compare commits

...
Author SHA1 Message Date
69d8b2a716 imsd 0.3.2
All checks were successful
package / package (push) Successful in 1m46s
Landline callers ring. The media leg plays AMR narrowband and G.711
(PCMA/PCMU) as well as AMR-WB, the engine accepts any of them from an
inbound offer (case-insensitive names, static G.711 payload types) and
names the offered codecs when it still has to refuse with 488. An inbound
INVITE is validated before it is announced to the UI, so a refused call no
longer flashes the dialer. Our own offer lists AMR narrowband after AMR-WB,
so a call to a landline no longer depends on the network transcoding up.
CODECS=<list> restricts and orders the codecs offered and accepted, for
bench work against networks that transcode every caller to AMR-WB.

Bench-verified on KPN 2026-09-08: PCMA and AMR-NB in both directions, the
default incoming path still AMR-WB, a mobile caller's offer still answered
with octet-aligned AMR-WB.

Packaging: cross-compiled for the FP6's Cortex-A520/A720 cores; the aport
is gone, fp6-img installs this repo's published apk.
2026-09-08 19:52:02 +02:00
c5cba2e7ff tests: dial a documentation-style number in the CODECS offer scenario 2026-09-08 19:51:24 +02:00
37ec8660b7 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.
2026-09-08 17:53:26 +02:00
452634683c packaging: cross-compile for the FP6's own cores
The phone is one SoC, 4x Cortex-A520 + 4x Cortex-A720, and the package is
built for nothing else, so build for it: -march=armv8.6-a+fp16fml+aes+sha3+sm4
-mtune=cortex-a720 instead of the generic Armv8 baseline. That is gcc's own
-march=native expansion on the phone, re-based on armv8.6-a so clang can
express the same set; 8.6 is the highest level whose mandatory set the phone
exposes (8.7 would assume WFxT, 9.x SVE2 - neither is in its hwcaps, and
SVE2 code would SIGILL). Same string as the fp6-img aports and kernel.
2026-09-06 00:02:31 +02:00
7 changed files with 277 additions and 70 deletions

View file

@ -110,7 +110,7 @@ make install # DESTDIR/PREFIX staged install incl. the packaging/ files
packaging/make-sysroot.sh # one-time: Alpine aarch64 sysroot from the CDN
crafter-build -- --target=aarch64-alpine-linux-musl \
--sysroot=$HOME/.cache/imsd/sysroot-aarch64-alpine \
--march=armv8-a --mtune=generic
--march=armv8.6-a+fp16fml+aes+sha3+sm4 --mtune=cortex-a720
# same flags after `crafter-build test --target=aarch64-alpine-linux-musl`
# run the suite under qemu-aarch64 against the sysroot
```
@ -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

@ -41,7 +41,7 @@ import Imsd;
namespace {
// ---- static config (env-overridable, same knobs as imsd.py) ---------------
constexpr const char* Version = "0.3.1";
constexpr const char* Version = "0.3.2";
constexpr const char* BusName = "net.catcrafts.IMS1";
constexpr const char* ObjPath = "/net/catcrafts/IMS1";
constexpr const char* Iface = "net.catcrafts.IMS1";
@ -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";
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

@ -75,7 +75,11 @@ retry "make sysroot" "$SRC/packaging/make-sysroot.sh" "$SYSROOT"
# host arch — the aarch64 binaries are additionally exercised by fp6-img's
# chroot `make check` whenever an image builds)
cd "$SRC"
XTARGET="--target=aarch64-alpine-linux-musl --sysroot=$SYSROOT --march=armv8-a --mtune=generic"
# The phone is one SoC, 4x Cortex-A520 + 4x Cortex-A720, so build for it.
# armv8.6-a is the highest level whose mandatory set it exposes (8.7 would
# assume WFxT, 9.x SVE2 - neither is in its hwcaps); +fp16fml+aes+sha3+sm4 are
# the optional extensions it has. Same string as the fp6-img aports.
XTARGET="--target=aarch64-alpine-linux-musl --sysroot=$SYSROOT --march=armv8.6-a+fp16fml+aes+sha3+sm4 --mtune=cortex-a720"
crafter-build -- $XTARGET
crafter-build -- --product=media $XTARGET
crafter-build -- --product=dialerd $XTARGET

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", "0101234567", 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;
}