Compare commits

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

View file

@ -41,7 +41,7 @@ import Imsd;
namespace { namespace {
// ---- static config (env-overridable, same knobs as imsd.py) --------------- // ---- static config (env-overridable, same knobs as imsd.py) ---------------
constexpr const char* Version = "0.3.2"; constexpr const char* Version = "0.3.1";
constexpr const char* BusName = "net.catcrafts.IMS1"; constexpr const char* BusName = "net.catcrafts.IMS1";
constexpr const char* ObjPath = "/net/catcrafts/IMS1"; constexpr const char* ObjPath = "/net/catcrafts/IMS1";
constexpr const char* Iface = "net.catcrafts.IMS1"; constexpr const char* Iface = "net.catcrafts.IMS1";
@ -571,15 +571,6 @@ 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", "");
@ -697,7 +688,6 @@ 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_;
@ -1148,7 +1138,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, codecs_); call_.emplace(ctx_, rng_, c.uni, c.number, rtpPort_, precond_, emergency);
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());
@ -1208,7 +1198,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_, codecs_); call_.emplace(ctx_, rng_, uni, imsd::engine::IncomingInvite{msg}, rtpPort_);
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

View file

@ -99,20 +99,15 @@ 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.
// `codecs`: the daemon's CODECS override — the codecs offered on an CallMachine(const imsd::msg::Context& ctx, imsd::util::Rng& rng, std::string uni, std::string number, int rtpPort, bool precond, bool emergency = false)
// 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), codecs_(std::move(codecs)), attemptSos_(emergency) { rtpPort_(rtpPort), precond_(precond), 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), codecs_(std::move(codecs)), direction_(Direction::Incoming), precond_(false), 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_);
@ -153,7 +148,6 @@ 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_)));
@ -164,9 +158,8 @@ 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; the CODECS // codecs imsd-media plays (AMR-WB, AMR, PCMA, PCMU) — is refused
// override narrows that set) — is refused with 488 up front, before // with 488 up front, before the UI ever rings; the log line names
// 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() {
@ -325,7 +318,6 @@ 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,
@ -437,7 +429,6 @@ 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";
@ -541,7 +532,7 @@ export namespace imsd::engine {
} }
MediaLeg ParseAnswer() const { MediaLeg ParseAnswer() const {
imsd::sdp::Answer ans = imsd::sdp::ParseAnswer(answerSdp_, codecs_); imsd::sdp::Answer ans = imsd::sdp::ParseAnswer(answerSdp_);
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;

View file

@ -5,18 +5,15 @@
/* /*
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 by default an AMR-WB (octet-aligned) + AMR + telephone-event The offer is a fixed AMR-WB (octet-aligned) + AMR + telephone-event shape
shape with an optional GSMA IR.92 QoS-precondition block; it is what with an optional GSMA IR.92 QoS-precondition block; it is what commercial IMS
commercial IMS cores expect from a VoLTE UE, and its exact bytes are pinned by cores expect from a VoLTE UE, and its exact bytes are pinned by tests/Sdp. The
tests/Sdp. The parser extracts only what the media plane needs: remote parser extracts only what the media plane needs: remote address/port, the
address/port, the negotiated payload type, the codec and octet-align mode — it negotiated payload type, the codec and octet-align mode — it reads answers
reads answers to our offers and inbound offers alike (same fields either way). to our offers and inbound offers alike (same fields either way). The codecs
The codecs it knows are the ones imsd-media can play: AMR-WB, AMR (narrowband) it knows are the ones imsd-media can play: AMR-WB, AMR (narrowband) and
and G.711 (PCMA/PCMU) — the last two are what a PSTN gateway offers when a 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 landline calls. BuildAnswer is the terminating side: it answers an inbound
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++.
@ -52,52 +49,20 @@ 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. An explicit `codecs` list // clock. Byte layout is pinned by tests/Sdp.
// 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";
@ -105,33 +70,18 @@ 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";
std::string pts; s += std::format("m=audio {} RTP/AVP 97 99 98 100\r\n", o.rtpPort);
for (const auto& c : codecs) pts += std::format(" {}", pt(c)); s += "b=AS:41\r\n";
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) { s += "a=rtpmap:97 AMR-WB/16000/1\r\n";
if (c == "AMR-WB") { s += "a=fmtp:97 octet-align=1;mode-change-capability=2;max-red=0\r\n";
s += "a=rtpmap:97 AMR-WB/16000/1\r\n"; s += "a=rtpmap:99 AMR/8000/1\r\n";
s += "a=fmtp:97 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 if (c == "AMR") { s += "a=rtpmap:98 telephone-event/16000\r\n";
s += "a=rtpmap:99 AMR/8000/1\r\n"; s += "a=fmtp:98 0-15\r\n";
s += "a=fmtp:99 octet-align=1;mode-change-capability=2;max-red=0\r\n"; s += "a=rtpmap:100 telephone-event/8000\r\n";
} else { s += "a=fmtp:100 0-15\r\n";
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=ptime:20\r\n";
s += "a=maxptime:240\r\n"; s += "a=maxptime:240\r\n";
if (o.precond) { if (o.precond) {
@ -194,16 +144,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 (kCodecs order). Octet-aligned wins // octet-align=1 > AMR > PCMA > PCMU. Octet-aligned wins within a codec
// within a codec because it is the media leg's native, call-proven // because it is the media leg's native, call-proven format
// format (bandwidth-efficient is supported but was the s56 static-audio // (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. A // octet-aligned pt keeps real calls on the battle-tested path. Wideband
// non-empty `prefs` replaces the codec order (and restricts acceptance // beats narrowband, AMR beats G.711 (a mobile-to-mobile call must never
// to the codecs named) — the CODECS override. With no acceptable codec // land on a G.711 leg just because the gateway listed it). With no
// at all, the first pt is used and codec comes back EMPTY — pretending // playable codec at all, the first pt is used and codec comes back EMPTY
// otherwise would defeat codec checks upstream. // — pretending otherwise would defeat codec checks upstream.
Answer ParseAnswer(std::string_view body, std::span<const std::string> prefs = {}) { Answer ParseAnswer(std::string_view body) {
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);
@ -234,29 +184,39 @@ 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");
}; };
struct Seen { int first = -1; int oa = -1; }; int wbOa = -1;
std::map<std::string, Seen> seen; // codec name -> first pt, first octet-aligned pt int wb = -1;
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 (std::ranges::find(kCodecs, name) == kCodecs.end()) continue; if (name == "AMR-WB") {
Seen& s = seen[name]; if (wb == -1) wb = p;
if (s.first == -1) s.first = p; if (wbOa == -1 && hasOctetAlign(p)) wbOa = p;
if (s.oa == -1 && name.starts_with("AMR") && hasOctetAlign(p)) s.oa = 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;
}
} }
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();
} }

View file

@ -75,11 +75,7 @@ retry "make sysroot" "$SRC/packaging/make-sysroot.sh" "$SYSROOT"
# host arch — the aarch64 binaries are additionally exercised by fp6-img's # host arch — the aarch64 binaries are additionally exercised by fp6-img's
# chroot `make check` whenever an image builds) # chroot `make check` whenever an image builds)
cd "$SRC" cd "$SRC"
# The phone is one SoC, 4x Cortex-A520 + 4x Cortex-A720, so build for it. XTARGET="--target=aarch64-alpine-linux-musl --sysroot=$SYSROOT --march=armv8-a --mtune=generic"
# 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 -- $XTARGET
crafter-build -- --product=media $XTARGET crafter-build -- --product=media $XTARGET
crafter-build -- --product=dialerd $XTARGET crafter-build -- --product=dialerd $XTARGET

View file

@ -772,62 +772,6 @@ 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", "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"); if (Failures == 0) std::println("Engine: all tests passed");
return Failures; return Failures;
} }

View file

@ -294,93 +294,6 @@ 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;
} }