Compare commits
7 changed files with 70 additions and 277 deletions
|
|
@ -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 |
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
|
||||||
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) {
|
||||||
|
|
@ -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) {
|
}
|
||||||
auto it = seen.find(std::string(name));
|
if (wbOa != -1 || wb != -1) {
|
||||||
if (it == seen.end()) return false;
|
a.payloadType = wbOa != -1 ? wbOa : wb;
|
||||||
a.payloadType = it->second.oa != -1 ? it->second.oa : it->second.first;
|
a.codec = "AMR-WB";
|
||||||
a.codec = std::string(name);
|
} else if (nbOa != -1 || nb != -1) {
|
||||||
return true;
|
a.payloadType = nbOa != -1 ? nbOa : nb;
|
||||||
};
|
a.codec = "AMR";
|
||||||
bool found = false;
|
} else if (pcma != -1) {
|
||||||
if (prefs.empty()) {
|
a.payloadType = pcma;
|
||||||
for (std::string_view n : kCodecs) if ((found = pick(n))) break;
|
a.codec = "PCMA";
|
||||||
|
} 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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue