diff --git a/README.md b/README.md index 2b631e6..8db01bc 100644 --- a/README.md +++ b/README.md @@ -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.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` # run the suite under qemu-aarch64 against the sysroot ``` @@ -210,4 +210,3 @@ 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 | diff --git a/implementations/main.cpp b/implementations/main.cpp index 8e4f6bb..43b77f1 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -41,7 +41,7 @@ import Imsd; namespace { // ---- 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* ObjPath = "/net/catcrafts/IMS1"; constexpr const char* Iface = "net.catcrafts.IMS1"; @@ -571,15 +571,6 @@ 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", ""); @@ -697,7 +688,6 @@ private: int pcscfPort_ = 5060; int rtpPort_ = 50004; bool precond_ = false; - std::vector codecs_; // CODECS override; empty = defaults // registration imsd::msg::Context ctx_; @@ -1148,7 +1138,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, codecs_); + call_.emplace(ctx_, rng_, c.uni, c.number, rtpPort_, precond_, emergency); CallInfo info{c.uni, c.number, "dialing", "outgoing", NowEpoch(), 0}; PostAdded(info); bool ok = Execute(call_->Start()); @@ -1208,7 +1198,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_, codecs_); + call_.emplace(ctx_, rng_, uni, imsd::engine::IncomingInvite{msg}, rtpPort_); callReply_ = reply_; Log(std::format("incoming call {} from {}", uni, call_->Number())); // Validate the offer BEFORE the UI hears of the call. A refused diff --git a/interfaces/Imsd-Engine.cppm b/interfaces/Imsd-Engine.cppm index b91eef1..ffa46f2 100644 --- a/interfaces/Imsd-Engine.cppm +++ b/interfaces/Imsd-Engine.cppm @@ -99,20 +99,15 @@ export namespace imsd::engine { public: // `emergency`: the shell classified the dialled string as an // emergency number — the first attempt targets urn:service:sos. - // `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 codecs = {}) + CallMachine(const imsd::msg::Context& ctx, imsd::util::Rng& rng, std::string uni, std::string number, int rtpPort, bool precond, bool emergency = false) : 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(); } - CallMachine(const imsd::msg::Context& ctx, imsd::util::Rng& rng, std::string uni, IncomingInvite in, int rtpPort, - std::vector codecs = {}) + CallMachine(const imsd::msg::Context& ctx, imsd::util::Rng& rng, std::string uni, IncomingInvite in, int 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"), invite_(in.invite) { number_ = imsd::sip::CallerId(invite_); @@ -153,7 +148,6 @@ 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 a; 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 // 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; the CODECS - // override narrows that set) — is refused with 488 up front, before - // the UI ever rings; the log line names + // codecs imsd-media plays (AMR-WB, AMR, PCMA, PCMU) — 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 OnInvite() { @@ -325,7 +318,6 @@ 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, @@ -437,7 +429,6 @@ export namespace imsd::engine { std::string uni_, number_; int rtpPort_; bool precond_; - std::vector codecs_; // CODECS override; empty = defaults Direction direction_ = Direction::Outgoing; CallState state_ = CallState::Dialing; std::string reason_ = "outgoing"; @@ -541,7 +532,7 @@ export namespace imsd::engine { } MediaLeg ParseAnswer() const { - imsd::sdp::Answer ans = imsd::sdp::ParseAnswer(answerSdp_, codecs_); + imsd::sdp::Answer ans = imsd::sdp::ParseAnswer(answerSdp_); MediaLeg leg; leg.remoteIp = ans.ip; leg.remotePort = ans.port < 0 ? 0 : ans.port; diff --git a/interfaces/Imsd-Sdp.cppm b/interfaces/Imsd-Sdp.cppm index 723720d..471e294 100644 --- a/interfaces/Imsd-Sdp.cppm +++ b/interfaces/Imsd-Sdp.cppm @@ -5,18 +5,15 @@ /* 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 -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 +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 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++. @@ -52,52 +49,20 @@ 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 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 ParseCodecList(std::string_view spec) { - std::vector 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(std::toupper(static_cast(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 - std::vector 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. 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. + // clock. Byte layout is pinned by tests/Sdp. std::string BuildOffer(const Offer& o) { - static const std::vector kDefaultOffer{"AMR-WB", "AMR"}; - const std::vector& 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"; @@ -105,33 +70,18 @@ export namespace imsd::sdp { s += "s=-\r\n"; s += std::format("c=IN {} {}\r\n", ipver, o.local); s += "t=0 0\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 += std::format("m=audio {} RTP/AVP 97 99 98 100\r\n", o.rtpPort); + s += "b=AS:41\r\n"; 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=rtpmap:97 AMR-WB/16000/1\r\n"; + s += "a=fmtp:97 octet-align=1;mode-change-capability=2;max-red=0\r\n"; + s += "a=rtpmap:99 AMR/8000/1\r\n"; + s += "a=fmtp:99 octet-align=1;mode-change-capability=2;max-red=0\r\n"; + s += "a=rtpmap:98 telephone-event/16000\r\n"; + s += "a=fmtp:98 0-15\r\n"; + s += "a=rtpmap:100 telephone-event/8000\r\n"; + s += "a=fmtp:100 0-15\r\n"; s += "a=ptime:20\r\n"; s += "a=maxptime:240\r\n"; if (o.precond) { @@ -194,16 +144,16 @@ export namespace imsd::sdp { } // Payload-type preference: AMR-WB with octet-align=1 > AMR-WB > AMR with - // 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 + // 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 // culprit) — when the peer offers both variants, taking the - // 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 prefs = {}) { + // 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) { 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); @@ -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); return line.contains("octet-align=1"); }; - struct Seen { int first = -1; int oa = -1; }; - std::map seen; // codec name -> first pt, first octet-aligned pt + int wbOa = -1; + int wb = -1; + int nbOa = -1; + int nb = -1; + int pcma = -1; + int pcmu = -1; for (int p : pts) { std::string name = CodecName(body, 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 (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; + } } - 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; + 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"; } else { - for (const std::string& n : prefs) if ((found = pick(n))) break; - } - if (!found) { a.payloadType = pts.front(); a.codec.clear(); } diff --git a/packaging/build-package.sh b/packaging/build-package.sh index ae45bf6..e035173 100755 --- a/packaging/build-package.sh +++ b/packaging/build-package.sh @@ -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 # chroot `make check` whenever an image builds) cd "$SRC" -# 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" +XTARGET="--target=aarch64-alpine-linux-musl --sysroot=$SYSROOT --march=armv8-a --mtune=generic" crafter-build -- $XTARGET crafter-build -- --product=media $XTARGET crafter-build -- --product=dialerd $XTARGET diff --git a/tests/Engine/main.cpp b/tests/Engine/main.cpp index 2ad2f25..b835679 100644 --- a/tests/Engine/main.cpp +++ b/tests/Engine/main.cpp @@ -772,62 +772,6 @@ 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; } diff --git a/tests/Sdp/main.cpp b/tests/Sdp/main.cpp index e7f68ca..5a70cc9 100644 --- a/tests/Sdp/main.cpp +++ b/tests/Sdp/main.cpp @@ -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"); } - // ---- 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 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 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 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 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; }