https://freeswitch.org/confluence/display/FREESWITCH/Codec+Negotiation
摘要
编码协商可能是一个令人困惑的主题。如果您不熟悉SDP(会话描述协议),那么这就增加了一层额外的神秘。如果你对这个话题感到陌生(或者只是对你所读过的和经历的东西感到困惑),那么希望通过这个简短的介绍,事情会变得更清楚一些。
首先,我们所说的“编解码协商”是什么意思?我们正在讨论是:每条腿将选择什么编码(codec)的处理过程。FreeSWITCH支持许多种编码。大部分的SIP终端也支持多种编码。而每条腿只能使用一种编码,因此,编码协商过程就是筛选过滤并确定最终选用编码的过程。这种“筛选过程”会引起混淆。这个过程是如何工作的?又如何修改它?本文将帮你回答这些问题。
FreeSWITCH的编码协商
FreeSWITCH支持两种基本的编解码协商模式:早期协商和晚期协商。早期协商意味着在FreeSWITCH和终端之间尽快协商编码,甚至在FreeSWITCH需要发送媒体(如铃声)或响应呼叫之前。这个过程甚至在来电到触发拨号计划之前就发生了。晚期协商意味着延迟编解码的选择处理,直到呼叫触发拨号计划之后,(能够)收集到更多的信息时才协商编码。这些附加信息可用于影响协商过程。我们接下来说明早期协商和晚期协商之间的区别。
考虑一下这个场景:
- Alice有一台支持G722和PCMU的电话
- Bob有一台支持PCMU和PCMA的电话
- FreeSWITCH针对外呼的编码偏好列表是:G722,G722.1,PCMU,PCMA,G729
(FS的这个编码表将提供给所有的外呼通话)
观察以下两个呼叫流程,看你能否发现A腿编码协商处理的差异:
- 早期协商
Alice (A)拨打Bob (B)的号码
A呼叫FS并提供两种编码:G722和PCMU
FS检查A的编码表,并立即为A腿选择了G722编码
FS呼叫B并提供编码表:G722,G722.1,PCMU,PCMA,G729
B检视编码表,并选择PCMU
FS将A和B桥接在一起
FS执行G722和PCMU的编码转换
- 携带“继承编码”集的晚期协商
Alice (A)拨打Bob (B)的号码
A呼叫FS并提供两种编码:G722和PCMU
FS检视这两种编码,但没有马上选择其中的一种
FS呼叫B并提供可选编码G722和PCMU
B检视编码集并选择PCMU
此时FS为A腿选择PCMU编码
FS将A和B桥接到一块
A与B通过PCMU通信(不需要额外的编码转换)
注意A腿编码协商在时间上的差异。早期协商模式下,FreeSWITCH立刻选择一种编码(G722)。它并不等待和关心外呼(B腿)过程发生了什么,因此A腿直接选择722编码。在A腿决定编码后,FreeSWITCH通过拨号计划传递这个呼叫,最终通过一个bridge app呼叫Bob的电话。为了初始化bridge,FreeSWITCH呼叫了B腿,并提供了一个相当大的编码表,这个表来源于FreeSWITCH的外呼编码偏好配置(这个表是可自定义的,详情后叙)。Bob的电话看到这个编码表,并从中选择第一个匹配的编码,在本例中就是PCMU。这时,双边的编码协商结束。
- A 腿使用 G722
- B 腿使用 PCMU
Bob的电话一回振铃信号,两条腿就被桥接在一起。FreeSWITCH必须将Alice的编解码器(G 722)转换为Bob的编解码器(PCMU),因为协商的结果是两种不同的编码。通常情况下,这是没问题的,但假设两部话机支持的编码集有交集,那你可能希望最终协商出相同的编码以减少CPU的消耗。这就是晚期协商起作用的地方。
如您所见,当FreeSWITCH为A腿选择一种编码而不知道B腿上设置了什么编码时,就很容易出现“编码失配”。编码“不匹配”并不总是坏事,但很多时候,让两条腿共享相同的编码是件好事。使用晚期协商和一种名为“继承编码”的技术,我们可以强制A腿使用B腿上协商的编码。(这个过程稍后会详细描述,目前我们只考虑基本概念。)
在我们的第二个例子中,基本的呼叫流是FreeSWITCH创建A腿,但不是立即协商编解码。相反,FreeSWITCH在执行拨号计划之后才开始协商A腿的编码。在这种情况下,拨号计划执行结果是桥接Bob的电话。在桥接过程中,FreeSWITCH与Bob的电话协商了一种编码器即PCMU。一旦在B腿上选择了编码,FreeSWITCH就回到Alice的话机上,告诉它我们将在A腿上使用PCMU。现在呼叫的两条腿就使用相同的编码,这正是我们想要的。
这就是早期协商和晚期协商的区别所在。请记住:简单地启用晚期协商并不会自动强制A腿继承B腿的编码。它只是允许“继承编码”作为协商编码的一种方式。晚期协商允许使用其他编码协商技巧。更多详情,请继续阅读。
早期协商
这是缺省的行为模式。
一般原则
- 当A腿呼叫FreeSWITCH时,它所提供的编码表将与相关SIP配置文件中的inbound-codec-prefs内容进行比较。一旦A提供的某个编码与FS允许的编码匹配,就会选择它为A腿的编码。如果A提供的编码中没有一个与允许的编码相匹配,则呼叫失败。
注:由于算法的原因,入局SDP中的编码顺序相对于inbound-codec-prefs配置的编码顺序具体优先权。
- 当FS呼叫B腿时,SIP配置项outbound-codec-prefs中的编码表被重新组织,方法是将上面A腿协商的编码推到表的顶部。显然,如果B不能接受编码表,结果就是呼叫失败。
例如:
A -------- GSM/PCMA/G729 --------> FS (allowing G729/PCMA/PCMU) -------- PCMA/G729/PCMU --------> B
发生了什么?
- A向FS建议编码表GSM/PCMA/G729
- FS根据其允许的编码表(inbound-codec-prefs中配置)按优先次序(SDP中的先后顺序)检查建议的编码表,并选择PCMA作为第一个授权编码,因此编解码表调整为:PCMA/G 729/PCMU
- FS将调整后的编码表提交给B
早期协商参数
- disable-transcoding
这是一个可以在出局SIP配置文件中设置的参数。
它将强制B腿(出局)使用A腿(入局)协商结果相同的编码。
若要设置此参数,请将以下行添加到所需的SIP配置文件中
<param name="disable-transcoding" value="true"/>
注意:通常误解的是,这个参数禁用FS中的编码转换能力。这是错误的。
这个参数只是更改出局编码以匹配入局端上的协商结果,从而达到不需转码的目的。
通过设置absolute_codec_string变量值为入局编码,也能达到相同的目的。
这个参数(仅在关闭晚期协商时适用)将从A腿获取协商后的编码,并将其作为在B腿的唯一选项(忽略任何其他编码设置),如果B腿不能使用相同的编码,则呼叫失败。(笔者实测,实际上在晚期协商模式下也是生效的)
- absolute_codec_string
这是一个通道变量,可以在拨号计划中设置,通常是在桥接前设置。
这个变量将强制B腿使用变量值指定的编码列表,而不用考虑其它任何因素。
例如:
<action application="export" data="nolocal:absolute_codec_string=PCMA,PCMU"/>
<action application="bridge" data="sofia/gateway/mygateway/mynumber"/>
或者
<action application="bridge" data="{absolute_codec_string='PCMA,PCMU'}sofia/gateway/mygateway/mynumber"/>
在后一种表达式中,请在编码表加上一对单引号,以保护参数解析的完整性,以逗号( ',' )为编码分割符,比如{var1=val1,var2=val2,absolute_codec_string='GSM,PCMU'}
注意:这个通道变量适用于disable-transcoding变量。换句话说,如果在出局SIP配置文件设置了disable-transcoding变量,并且在拨号计划中设置absolute_codec_string
为有别于入局编码的值,那么依然会产生转码开销。
- codec_string
这是一个通道变量,可以在拨号计划中设置,通常是在桥接前设置。
The defined codec list will override the one set in the outbound-codec-prefs parameter of the outbound profile.
这个变量定义的编码列表将覆盖出局配置文件的 outbound-codec-prefs参数中设置的值。
以下是一个例子:
<action application="export" data="nolocal:codec_string=PCMA,PCMU"/>
<action application="bridge" data="sofia/gateway/mygateway/mynumber"/>
或者:
<action application="bridge" data="{codec_string='PCMA,PCMU'}sofia/gateway/mygateway/mynumber"/>
早期协商+禁用转码:
<param name="disable-transcoding" value="true"/>
如果sofia配置文件中设置了这个参数,那么所有呼出到B腿的信令中,将只携带A腿协商结果(只带一种编码,优先级最高的那个)
在任何一种情况下,A腿上的变量“codec_string”控制提供给B腿的编码集。
变量“absolute_codec_string”类似,但它暗示了隐式的编码列表,并将禁用一种默认行为--将A腿编码添加到列表中。
晚期协商(需要的参数)
- inbound-late-negotiation
<param name="inbound-late-negotiation" value="true"/>
- 呼叫将直接执行拨号计划而不需要先查看编码。
- 协商将在A腿被应答后进行。
- 允许你将呼叫路由到一个脚本,检查SDP,并通过"codec_string"通道变量重写可接受的编码表。
- inherit_codec
<action application="set" data="inherit_codec=true"/>
inherit_codec=true (仅在启用晚期协商时才适用)将在B腿应答后触发编码协商,界时B腿的协商结果将会传递给A腿,以便两边使用相同的编码(如果A腿支持它);否则,它将从自己的列表中使用它所能使用的一切
只有在配置文件上启用晚期协商时,此变量才可用。它是一个可读字符串,包含主叫终端提取的所有编码。这可以很容易地从拨号计划中解析出来。
<action application="export" data="codec_string='${ep_codec_string}'"/>
代理媒体
如果启用代理媒体并且没有执行过answer app,那么将无法进行编码协商。
不相容宿端
如下所示,在需要混频/双腿编码不同、特别是激活media_mix_inbound_outbound_codecs变量的场景,媒体代理都不适用。
混频/双腿编码不同(需要转码)
如果您希望FreeSWITCH能够将两条腿与不同的编码匹配(在其中进行编码转换),那么需要设置几个变量才能使其成为可能。首先,您必须设置media_mix_inbound_outbound_codecs=true,既可以全局设置(通过ie vars.xml),也可以通过bridge变量单独设置(例如:<action application="bridge" data="{media_mix_inbound_outbound_codecs=true}sofia/gateway/...."> )。
这个变量在V1.6及更高版本中是必须的,这是由于WebRTC支持,以及UDP承载SIP时SDP体量不断增长的原因。由于SDP的剪切尺寸,使得从WebRTC到SIP转换成原始SDP变得越来越困难。基于UDP承载SIP时,一旦出现分包,就会破坏SIP数据包,因为UDP不会进行自动分包重组。当SIP包大于MTU值时,就会产生分包,唯一的解决方案就是减小包的大小。这意味着压缩报头、消除额外的报头、或者删减不需要的编码描述以裁减SDP包体大小。
这意味着紧凑的头,消除额外的头,或只是不使用这么大的SDP消除编解码你不需要。对FS的影响是:如果没有将media_mix_inbound_outbound_codecs设置为true,则只有A腿提供给FS的编码会被传递给B腿,这会减小SDP的体量,并大大减小了转码所带来的副作用。
其次,你可能希望关闭inbound-late-negotiation和inbound-zrtp-passthru (第二个强制第一个为true),或者希望在桥接之前预先应答A腿的呼叫。
最后,您应该确保这些变量,disable-transcoding、,inherit_codec、,bypass_media、proxy_media,统统设置为false (因为这些都与我们想要实现的目标无关)。
警告
如果您的sdp包含具有不同ptime首选项的编码,并且ptime在sdp中指定,那么sofia将不会发送ptime属性
2010-10-18 11:47:52.234322 [WARNING] sofia_glue.c:213 Codec G723 payload 4 added to sdp wanting ptime 30 but it's already 20 (G729:18:20), disabling ptime.
说明实例
使用代理媒体模式时修改编码
下面是一个dialplan示例,它允许您更改编码首选顺序,同时仍然享受proxy_media模式的低cpu使用率。它类似于openser/opensips/kamailio 的textops模块中的subst()。
<context>
<extension name="sdp_mangler">
<!-- Set some defaults for this call -->
<condition field="destination_number" expression="^(\d+)$" break="on-false">
<action application="set" data="transfer_to=${destination_number} XML fail"/>
</condition>
<!-- Here we check for G729 if found we are going make sure we have it first in preference order -->
<condition field="${switch_r_sdp}" expression="/(.*)(m=audio \d+ RTP\/AVP)(?=[ \d]+18|18[ \d]+)([ \d]*)(.*)/s" break="never">
<action application="set" data="codec_mangle= 18"/>
<action application="set" data="transfer_to=${destination_number} XML public_proxy"/>
</condition>
<!-- Here we check for PCMU to add it back into the SDP if we want -->
<condition field="${switch_r_sdp}" expression="/(.*)(m=audio \d+ RTP\/AVP)(?=[ \d]+0|0[ \d]+)([ \d]*)(.*)/s" break="never">
<action application="set" data="codec_mangle=${codec_mangle} 0"/>
</condition>
<!-- Here we rebuild our SDP Moving G729 up front-->
<condition field="${switch_r_sdp}" expression="/(.*)(m=audio \d+ RTP\/AVP)([ \d]+)(.*)/s" break="never">
<action application="set" data="switch_r_sdp=$1$2${codec_mangle} 101$4"/>
</condition>
<condition>
<action application="transfer" data="${transfer_to}"/>
</condition>
</extension>
</context>
重写SDP
switch_r_sdp:
<action application="set">
<![CDATA[switch_r_sdp=(sdp here)
]]>
</action>
出局禁用G.729b
<extension name="disable-annexB" continue="true">
<condition field="${switch_r_sdp}" expression="/(.*)(m=audio \d+ RTP\/AVP)(.*)( 18 )(.*)/s">
<action application="export" data="rtp_append_audio_sdp=a=fmtp:18 annexb=no"/>
</condition>
</extension>
启用代理媒体时的编码协商
- 正确方式
<extension name="test_proxy_media">
<condition field="source" expression="mod_sofia">
<action application="answer"/>
<action application="playback" data="ivr/ivr-welcome_to_freeswitch.wav"/>
</condition>
</extension>
- 错误方式
<extension name="test_proxy_media">
<condition field="source" expression="mod_sofia">
<action application="playback" data="ivr/ivr-welcome_to_freeswitch.wav"/>
</condition>
</extension>