From 59e83b5006a71e975c8e22dbcaea3eaabb1d089a Mon Sep 17 00:00:00 2001 From: Vadim Saranov Date: Wed, 9 Sep 2026 09:42:25 +0300 Subject: [PATCH] core: SDP bandwidth modifiers (b=AS/RS/RR) support SEMS parsed b= lines in incoming SDP and copied them into generated SDP, but could not manage them: there was no way to add or compute bandwidth modifiers for offers SEMS generates, which 3GPP interoperability (VoLTE/RCS, TS 26.114 6.2.7.2) often requires. Add AmSdpBandwidth: computes b=AS from the selected codec and a=ptime (including IP/UDP/RTP overhead) and derives b=RS/RR per RFC 3556 defaults (1.25% / 3.75%). Controlled by a global config parameter sdp_bandwidth={off|auto} (off by default, preserving current behavior) and a per-session SdpBandwidthCtl override for applications. In 'auto' mode: - in answers, b= lines received in the offer are echoed back; - in offers initiated by SEMS, b=AS/RS/RR are computed from the negotiated codec and ptime; no value is generated for codecs not covered by the internal rate table; - relayed (B2BUA) bodies always pass the endpoints' b= lines through unchanged, independent of the global setting. Signed-off-by: Vadim Saranov --- core/AmB2BSession.cpp | 12 ++ core/AmConfig.cpp | 13 +++ core/AmConfig.h | 11 ++ core/AmSdp.cpp | 30 ++++- core/AmSdp.h | 4 + core/AmSdpBandwidth.cpp | 240 ++++++++++++++++++++++++++++++++++++++ core/AmSdpBandwidth.h | 98 ++++++++++++++++ core/AmSession.cpp | 10 ++ core/AmSession.h | 9 ++ core/etc/sems.conf.sample | 18 +++ 10 files changed, 442 insertions(+), 3 deletions(-) create mode 100644 core/AmSdpBandwidth.cpp create mode 100644 core/AmSdpBandwidth.h diff --git a/core/AmB2BSession.cpp b/core/AmB2BSession.cpp index 5466c698..7fed2a2b 100644 --- a/core/AmB2BSession.cpp +++ b/core/AmB2BSession.cpp @@ -846,6 +846,18 @@ void AmB2BSession::updateLocalBody(AmMimeBody& body) updateLocalSdp(parser_sdp); updateLocalSdpOrigin(parser_sdp); + // Relayed b= lines (bandwidth modifiers) pass through unchanged, so the + // A-leg b=AS/RS/RR are echoed to the B-leg. Only an explicit per-call + // control (SdpBandwidthCtl) modifies them. + if (sdp_bw_ctl.mode != SdpBandwidthCtl::Inherit) { + for (vector::iterator it = parser_sdp.media.begin(); + it != parser_sdp.media.end(); ++it) { + if (sdp_bw_ctl.mode == SdpBandwidthCtl::Auto && !it->bandwidth.empty()) + continue; // keep the passed-through b= lines + AmSdpBandwidth::apply(*it, sdp_bw_ctl, NULL); + } + } + // regenerate SDP string n_body; parser_sdp.print(n_body); diff --git a/core/AmConfig.cpp b/core/AmConfig.cpp index cf2a33bc..33121f2c 100644 --- a/core/AmConfig.cpp +++ b/core/AmConfig.cpp @@ -108,6 +108,7 @@ int AmConfig::UnhandledReplyLoglevel = 0; bool AmConfig::SkipGenerateDirectionBoth = false; bool AmConfig::IgnoreRouteSetOnPrack = false; +AmConfig::SdpBandwidthMode AmConfig::SdpBandwidth = AmConfig::SdpBwOff; #ifdef WITH_ZRTP bool AmConfig::enable_zrtp = true; @@ -417,6 +418,18 @@ int AmConfig::readConfiguration() IgnoreRouteSetOnPrack = (cfg.getParameter("ignore_routeset_on_prack") == "yes"); } + if(cfg.hasParameter("sdp_bandwidth")) { + const string& sdp_bandwidth = cfg.getParameter("sdp_bandwidth"); + if (sdp_bandwidth == "off") { + SdpBandwidth = SdpBwOff; + } else if (sdp_bandwidth == "auto") { + SdpBandwidth = SdpBwAuto; + } else { + ERROR("Unknown value of sdp_bandwidth parameter '%s', using 'off'\n", + sdp_bandwidth.c_str()); + } + } + if(cfg.hasParameter("sip_nat_handling")) { SipNATHandling = (cfg.getParameter("sip_nat_handling") == "yes"); } diff --git a/core/AmConfig.h b/core/AmConfig.h index 58c63bc4..105444a0 100644 --- a/core/AmConfig.h +++ b/core/AmConfig.h @@ -207,12 +207,23 @@ struct AmConfig static vector CodecOrder; static bool SkipGenerateDirectionBoth; + /** strict RFC 3261 behavior for the 200 OK on PRACK: when true, the route * set is not updated (it is taken once when the dialog is created) and the * remote tag is only taken from the 200 OK on PRACK if it was not yet set. * false (default) keeps the legacy behavior of updating both. */ static bool IgnoreRouteSetOnPrack; + /** SDP bandwidth modifiers (b=AS/RS/RR) for audio media SEMS generates: + * off - do not add bandwidth modifiers (default); + * auto - in answers echo the b= lines received in the offer; in own + * offers compute b=AS from codec + ptime and b=RS/RR from AS. + * Per-session override is available through SdpBandwidthCtl (AmSession). + * Relayed (B2BUA) bodies pass the endpoints' b= lines through unchanged, + * independent of this setting. */ + enum SdpBandwidthMode { SdpBwOff = 0, SdpBwAuto }; + static SdpBandwidthMode SdpBandwidth; + enum ApplicationSelector { App_RURIUSER, App_RURIPARAM, diff --git a/core/AmSdp.cpp b/core/AmSdp.cpp index 500095b8..cf93872d 100644 --- a/core/AmSdp.cpp +++ b/core/AmSdp.cpp @@ -349,7 +349,8 @@ AmSdp::AmSdp(const AmSdp& p_sdp_msg) sessionName(p_sdp_msg.sessionName), conn(p_sdp_msg.conn), media(p_sdp_msg.media), - attributes(p_sdp_msg.attributes) + attributes(p_sdp_msg.attributes), + bandwidth(p_sdp_msg.bandwidth) { } @@ -556,6 +557,12 @@ void AmSdp::print(string& body) const out_buf += "t=0 0\r\n"; + // add bandwidth (session level) + for (std::vector::const_iterator bw_it = + bandwidth.begin(); bw_it != bandwidth.end(); bw_it++) { + out_buf += "b=" + *bw_it + CRLF; + } + // add attributes (session level) for (std::vector::const_iterator a_it= attributes.begin(); a_it != attributes.end(); a_it++) { @@ -616,6 +623,12 @@ void AmSdp::print(string& body) const out_buf += "\r\nc=IN " + addr_t_2_str(media_it->conn.addrType) + " " + media_it->conn.address; + // add bandwidth (media level) + for (std::vector::const_iterator bw_it = + media_it->bandwidth.begin(); bw_it != media_it->bandwidth.end(); bw_it++) { + out_buf += "\r\nb=" + *bw_it; + } + out_buf += "\r\n" + options; if(media_it->send){ @@ -829,6 +842,7 @@ void AmSdp::clear() uri.clear(); conn = SdpConnection(); attributes.clear(); + bandwidth.clear(); media.clear(); l_origin = SdpOrigin(); } @@ -1006,13 +1020,20 @@ static bool parse_sdp_line_ex(AmSdp* sdp_msg, const char * s) case 'i': case 'e': case 'p': - case 'b': case 't': case 'k': s = is_eql_next(s); s = skip_till_next_line(s, line_len); state = SDP_DESCR; break; + case 'b': + s = is_eql_next(s); + next = skip_till_next_line(s, line_len); + if (line_len) + sdp_msg->bandwidth.push_back(string(s, line_len)); + s = next; + state = SDP_DESCR; + break; case 'a': s = is_eql_next(s); parse_session_attr(sdp_msg, s, &next); @@ -1075,7 +1096,10 @@ static bool parse_sdp_line_ex(AmSdp* sdp_msg, const char * s) case 'b': s = is_eql_next(s); - s = skip_till_next_line(s, line_len); + next = skip_till_next_line(s, line_len); + if (line_len && !sdp_msg->media.empty()) + sdp_msg->media.back().bandwidth.push_back(string(s, line_len)); + s = next; state = SDP_MEDIA; break; diff --git a/core/AmSdp.h b/core/AmSdp.h index 277cafec..e93cf525 100644 --- a/core/AmSdp.h +++ b/core/AmSdp.h @@ -247,6 +247,8 @@ struct SdpMedia std::vector attributes; // unknown attributes + std::vector bandwidth; // b= lines (e.g. "AS:29", "RR:1087") + std::vector iceCandidates; bool operator == (const SdpMedia& other) const; @@ -318,6 +320,8 @@ public: SdpConnection conn; // c= std::vector attributes; // unknown session level attributes + std::vector bandwidth; // session-level b= lines + std::vector media; // m= ... [a=rtpmap:...]+ //TODO: t= lines diff --git a/core/AmSdpBandwidth.cpp b/core/AmSdpBandwidth.cpp new file mode 100644 index 00000000..20a246a3 --- /dev/null +++ b/core/AmSdpBandwidth.cpp @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2026 Vadim Saranov + * + * This file is part of SEMS, a free SIP media server. + * + * SEMS is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. This program is released under + * the GPL with the additional exemption that compiling, linking, + * and/or using OpenSSL is allowed. + * + * For a license to use the SEMS software under conditions + * other than those described here, or to purchase support for this + * software, please contact iptel.org by e-mail at the following addresses: + * info@iptel.org + * + * SEMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +/** @file AmSdpBandwidth.cpp - SDP bandwidth modifiers (b=AS/RS/RR) */ + +#include +#include +#include + +#include "AmSdpBandwidth.h" +#include "AmConfig.h" + +static inline int ceil_div(int a, int b) { return (a + b - 1) / b; } + +/** speech bits per channel mode, see 3GPP TS 26.171 (AMR-NB) / + TS 26.201 / TS 26.202 (AMR-WB), per 20 ms frame */ +static const int amr_nb_bits[8] = { 95, 103, 118, 134, 148, 159, 204, 244 }; +static const int amr_wb_bits[9] = { 132, 177, 261, 285, 317, 365, 397, 461, 477 }; + +/** + * Get a codec parameter from an fmtp string ("key=value" entries separated + * by ';'). Returns false if the parameter is not present. + */ +static bool get_fmtp_param(const string& fmtp, const char* key, string& val) +{ + size_t klen = strlen(key); + size_t pos = 0; + while (pos < fmtp.size()) { + size_t end = fmtp.find(';', pos); + if (end == string::npos) end = fmtp.size(); + size_t s = pos; + while (s < end && isspace(fmtp[s])) s++; + size_t e = end; + while (e > s && isspace(fmtp[e-1])) e--; + if (e > s + klen && fmtp.compare(s, klen, key, klen) == 0 + && fmtp[s+klen] == '=') { + val = fmtp.substr(s+klen+1, e-s-klen-1); + return true; + } + pos = end + 1; + } + return false; +} + +/** + * Highest AMR/AMR-WB channel mode from the "mode-set" fmtp parameter + * ("0,1,2" or "0-7" forms). Defaults to the highest supported mode. + */ +static int amr_max_mode(const string& fmtp, int n_modes) +{ + string ms; + if (!get_fmtp_param(fmtp, "mode-set", ms) || ms.empty()) + return n_modes - 1; + + int max_mode = -1; + size_t pos = 0; + while (pos < ms.size()) { + size_t end = ms.find(',', pos); + if (end == string::npos) end = ms.size(); + int lo = atoi(ms.substr(pos, end-pos).c_str()); + size_t dash = ms.find('-', pos); + int hi = lo; + if (dash != string::npos && dash < end) + hi = atoi(ms.substr(dash+1, end-dash-1).c_str()); + if (lo > max_mode) max_mode = lo; + if (hi > max_mode) max_mode = hi; + pos = end + 1; + } + + if (max_mode < 0 || max_mode >= n_modes) + return n_modes - 1; + return max_mode; +} + +/** + * AMR/AMR-WB RTP payload size in bytes for one ptime interval. + * Bandwidth-efficient framing (default) adds CMR (4) + ToC (6) bits; + * octet-aligned framing adds a single ToC byte (octets-pc mode is ignored). + */ +static int amr_frame_bytes(const int* bits_table, int n_modes, + const string& fmtp) +{ + int bits = bits_table[amr_max_mode(fmtp, n_modes)]; + + string oa; + if (get_fmtp_param(fmtp, "octet-align", oa) && atoi(oa.c_str()) == 1) + return ceil_div(bits, 8) + 1; + + return ceil_div(bits + 10, 8); +} + +/** + * RTP payload (no RTP/UDP/IP headers) size in bytes for one ptime interval. + * @return bytes or -1 for codecs not covered by the table + */ +static int codec_payload_bytes(const SdpPayload& p, int ptime) +{ + string name = p.encoding_name; + for (size_t i = 0; i < name.size(); i++) + name[i] = toupper(name[i]); + + if (name == "PCMU" || name == "PCMA" || name == "G722") + return 8 * ptime; // 64 kbit/s + + if (name == "L16" && p.clock_rate > 0 && (p.clock_rate % 1000) == 0) + return p.clock_rate / 1000 * 2 * ptime; // 16 bit/sample, mono + + if (name == "GSM") + return 33 * ceil_div(ptime, 20); // 33 B / 20 ms frame + + if (name == "ILBC") { + string mode_s; + int mode = (get_fmtp_param(p.sdp_format_parameters, "mode", mode_s) + && atoi(mode_s.c_str()) == 30) ? 30 : 20; + return (mode == 30 ? 50 : 38) * ceil_div(ptime, mode); + } + + if (name == "AMR") + return amr_frame_bytes(amr_nb_bits, 8, p.sdp_format_parameters); + + if (name == "AMR-WB") + return amr_frame_bytes(amr_wb_bits, 9, p.sdp_format_parameters); + + if (name == "EVS" && p.clock_rate == 16000) { + string br_s; + int bitrate = 16400; + if (get_fmtp_param(p.sdp_format_parameters, "max-bitrate", br_s)) { + int v = atoi(br_s.c_str()); + if (v > 0) bitrate = v; + } + // 10 ms frames: bitrate/100 bits per frame, padded to a byte boundary + // + 1 class byte, see 3GPP TS 26.445/26.448 + return ceil_div(ptime, 10) * (ceil_div(bitrate, 800) + 1); + } + + return -1; +} + +int AmSdpBandwidth::computeAsKbps(const SdpMedia& m) +{ + const SdpPayload* sel = NULL; + for (std::vector::const_iterator it = m.payloads.begin(); + it != m.payloads.end(); ++it) { + if (it->encoding_name == "telephone-event") + continue; + sel = &*it; + break; + } + if (sel == NULL || sel->encoding_name.empty()) + return -1; + + int ptime = 20; + for (std::vector::const_iterator it = m.attributes.begin(); + it != m.attributes.end(); ++it) { + if (it->attribute == "ptime") { + int v = atoi(it->value.c_str()); + if (v > 0) ptime = v; + break; + } + } + + int payload_bytes = codec_payload_bytes(*sel, ptime); + if (payload_bytes < 0) + return -1; + + // IP (20) + UDP (8) + RTP (12); RTCP excluded (TS 26.114 6.2.7.2) + int total_bytes = payload_bytes + 40; + + // kbit/s = bytes * 8 / ptime, rounded up + return ceil_div(total_bytes * 8, ptime); +} + +void AmSdpBandwidth::apply(SdpMedia& m, const SdpBandwidthCtl& ctl, + const SdpMedia* offer) +{ + m.bandwidth.clear(); + + if (m.type != MT_AUDIO) + return; + + // b=AS describes the maximum bandwidth for the receiving direction + // (3GPP TS 26.114 6.2.7.2): do not emit for sendonly/inactive media. + if (!m.recv) + return; + + SdpBandwidthCtl::Mode mode = ctl.mode; + if (mode == SdpBandwidthCtl::Inherit) + mode = (AmConfig::SdpBandwidth == AmConfig::SdpBwAuto) + ? SdpBandwidthCtl::Auto : SdpBandwidthCtl::None; + + if (mode == SdpBandwidthCtl::None) + return; + + if (mode == SdpBandwidthCtl::Custom) { + if (ctl.as >= 0) m.bandwidth.push_back("AS:" + std::to_string(ctl.as)); + if (ctl.rr >= 0) m.bandwidth.push_back("RR:" + std::to_string(ctl.rr)); + if (ctl.rs >= 0) m.bandwidth.push_back("RS:" + std::to_string(ctl.rs)); + return; + } + + // Auto: in an answer echo the offer's b= lines as-is (no recomputation + // on codec downgrade); in a locally generated offer compute AS+RS+RR. + if (offer != NULL && !offer->bandwidth.empty()) { + m.bandwidth = offer->bandwidth; + return; + } + + int as = computeAsKbps(m); + if (as < 0) + return; // no value is invented for codecs not covered by the table + + m.bandwidth.push_back("AS:" + std::to_string(as)); + int rr = computeRr(as); + int rs = computeRs(as); + if (rr > 0) m.bandwidth.push_back("RR:" + std::to_string(rr)); + if (rs > 0) m.bandwidth.push_back("RS:" + std::to_string(rs)); +} diff --git a/core/AmSdpBandwidth.h b/core/AmSdpBandwidth.h new file mode 100644 index 00000000..9a312fb1 --- /dev/null +++ b/core/AmSdpBandwidth.h @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2026 Vadim Saranov + * + * This file is part of SEMS, a free SIP media server. + * + * SEMS is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. This program is released under + * the GPL with the additional exemption that compiling, linking, + * and/or using OpenSSL is allowed. + * + * For a license to use the SEMS software under conditions + * other than those described here, or to purchase support for this + * software, please contact iptel.org by e-mail at the following addresses: + * info@iptel.org + * + * SEMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +/** @file AmSdpBandwidth.h - SDP bandwidth modifiers (b=AS/RS/RR) */ + +#ifndef __AmSdpBandwidth_h__ +#define __AmSdpBandwidth_h__ + +#include +#include + +#include "AmSdp.h" + +/** + * Per-session control of SDP bandwidth modifiers (b=AS/RS/RR, RFC 4566/3556). + * + * Four states: + * Inherit - resolved through global AmConfig::SdpBandwidth + * None - never emit b= lines for this session + * Auto - echo b= from the offer if present, otherwise compute + * b=AS from codec + ptime and RS/RR from AS + * Custom - explicit values (as in kbps, rr/rs in bps, -1 = unset) + * + * Precedence: session control > global config. Applications are expected to + * set this per session/call. + */ +struct SdpBandwidthCtl { + enum Mode { Inherit = 0, None, Auto, Custom }; + + Mode mode; + int as; // kbps, -1 unset + int rr; // bps, -1 unset + int rs; // bps, -1 unset + + SdpBandwidthCtl() : mode(Inherit), as(-1), rr(-1), rs(-1) {} +}; + +class AmSdpBandwidth +{ +public: + /** + * Computes b=AS (application maximum bandwidth, kbps, integer) for a + * media descriptor from its first non-telephony payload (codec name, + * clock rate and fmtp) and a=ptime (default 20 ms). + * + * AS includes IP(20)/UDP(8)/RTP(12) overhead and excludes RTCP + * (3GPP TS 26.114 6.2.7.2, receiving direction). + * + * @return AS in kbps or -1 if the codec is not covered + * (no value is invented for unknown codecs). + */ + static int computeAsKbps(const SdpMedia& m); + + /** RTCP bandwidth from b=AS, RFC 3556 default shares: + b=RS = 1.25% and b=RR = 3.75% of the session bandwidth, in bps. */ + static int computeRs(int as_kbps) { return as_kbps < 0 ? -1 : as_kbps * 125 / 10; } + static int computeRr(int as_kbps) { return as_kbps < 0 ? -1 : as_kbps * 375 / 10; } + + /** + * Applies the bandwidth policy to a media descriptor, replacing all + * of its b= lines. + * + * @param m media to modify (answer or locally generated offer) + * @param ctl per-session control + * @param offer matching offer media to echo b= from, or nullptr + * + * b= lines describe the maximum bandwidth for the receiving direction, + * so nothing is applied for media we do not receive (sendonly/inactive). + * Audio media only. + */ + static void apply(SdpMedia& m, const SdpBandwidthCtl& ctl, + const SdpMedia* offer); +}; + +#endif diff --git a/core/AmSession.cpp b/core/AmSession.cpp index d2ec566f..4a8d730f 100644 --- a/core/AmSession.cpp +++ b/core/AmSession.cpp @@ -990,6 +990,12 @@ bool AmSession::getSdpOffer(AmSdp& offer) // RTPStream()->setLocalIP(localMediaIP()); // RTPStream()->getSdpOffer(media_idx,offer.media.back()); + // SDP bandwidth modifiers (b=AS/RS/RR) for own offers + for (vector::iterator it = offer.media.begin(); + it != offer.media.end(); ++it) { + AmSdpBandwidth::apply(*it, sdp_bw_ctl, NULL); + } + return true; } @@ -1113,6 +1119,10 @@ bool AmSession::getSdpAnswer(const AmSdp& offer, AmSdp& answer) // sort payload type in the answer according to the priority given in the codec_order configuration key std::stable_sort(answer_media.payloads.begin(),answer_media.payloads.end(),codec_priority_cmp()); + + // SDP bandwidth modifiers (b=AS/RS/RR): echo from offer or compute + AmSdpBandwidth::apply(answer_media, sdp_bw_ctl, &*m_it); + // we have one accepted audio stream, reject the others audio_1st_stream = false; } else { diff --git a/core/AmSession.h b/core/AmSession.h index c499529f..dbf7be94 100644 --- a/core/AmSession.h +++ b/core/AmSession.h @@ -41,6 +41,7 @@ #include "AmSessionEventHandler.h" #include "AmMediaProcessor.h" #include "AmRtpTransport.h" +#include "AmSdpBandwidth.h" #include #include @@ -227,6 +228,9 @@ public: bool active_rtp_stream_i; int active_media_index; + /** per-session SDP bandwidth modifiers (b=AS/RS/RR) control */ + SdpBandwidthCtl sdp_bw_ctl; + AmRtpTransport* createRtpTransport(AmRtpStream* stream, const string& ip, bool rtcp_mux, bool ice, bool srtp); @@ -612,6 +616,11 @@ public: /** Hook called when an SDP offer is required */ virtual bool getSdpAnswer(const AmSdp& offer, AmSdp& answer); + /** Per-session control of SDP bandwidth modifiers (b=AS/RS/RR). + * Allows applications (e.g. lua_sems) to override the global + * sdp_bandwidth configuration per call. */ + SdpBandwidthCtl& getSdpBandwidthCtl() { return sdp_bw_ctl; } + /** Hook called when an SDP OA transaction has been completed */ virtual int onSdpCompleted(const AmSdp& offer, const AmSdp& answer); diff --git a/core/etc/sems.conf.sample b/core/etc/sems.conf.sample index 4bcd5847..bc6a8252 100644 --- a/core/etc/sems.conf.sample +++ b/core/etc/sems.conf.sample @@ -154,6 +154,24 @@ rtp_high_port=60000 # #force_symmetric_rtp=yes +# SDP bandwidth modifiers: sdp_bandwidth={off|auto} +# +# Controls b=AS/RS/RR bandwidth modifiers in SDP media (audio) generated +# by SEMS (b= lines describe the maximum bandwidth for the receiving +# direction, 3GPP TS 26.114 6.2.7.2): +# off - SEMS does not add bandwidth modifiers; +# auto - in answers the b= lines received in the offer are echoed back; +# in offers initiated by SEMS b=AS is computed from the offered +# codec and a=ptime (incl. IP/UDP/RTP overhead), and b=RS/RR +# from b=AS (RFC 3556 defaults: 1.25% / 3.75%). +# No value is generated for codecs not covered by the internal rate table. +# Relayed (B2BUA) bodies always pass the endpoints' b= lines through +# unchanged. Per-call control is available to applications (SdpBandwidthCtl). +# +# default: off +# +#sdp_bandwidth=off + # optional parameter: outbound_proxy=uri # # - this sets an outbound proxy for dialogs and registrations initiated