最近在修改asterisk转码和编码协商的问题,发现asterisk的转码策略的选择还是有些问题的(基于1.8.9.3版本)。
——————————————
相关的CLI命令
转码路径的调试命令:
core show channels
core show channel ${CHANNEL}
查看不同编码之间进行转换的时间开销:
core show translation
查看某种编码转换为其它编码的路径:
core show translation paths {codec}
eg: core show translation paths ulaw
ast_channel中与转码相关的数据成员:
ast_channel->nativeformats
ast_channel->writeformat
ast_channel->readformat
ast_channel->rawwriteformat
ast_channel->rawreadformat
ast_channel->writetrans
ast_channel->readtrans
——————————————
以下是测试用的case:
phone A: PCMU phone B:GSM
user A:PCMU,GSM user B:PCMA,GSM
对于该用例asterisk的转码路径是这样的。
1.phone A => phone B
channel A读转码(PCMU => PCMA)
channel B写转码(PCMA => SLINEAR => GSM)
2.phone B => phone A
channel B读转码(GSM => SLINEAR => PCMU)
channel A写转码(PCMU,无需转码)
从主叫到被叫与从被叫到主叫的转码路径是不一致的,前者比后者多了一次从 PCMU 到 PCMA 的转换。为什么会出现这种情况呢?
打开log开关、结合CLI命令进行分析,开始看代码。
asterisk对于是否需要转码及转码策略的选择是在ast_channel_make_compatible中做的。该函数又调用ast_channel_make_compatible_helper来设置从主叫到被叫及被叫到主叫的转码策略。
int ast_channel_make_compatible(struct ast_channel *chan, struct ast_channel *peer) { /* Some callers do not check return code, and we must try to set all call legs correctly */ int rc = 0; /* Set up translation from the chan to the peer */ // modify //rc = ast_channel_make_compatible_helper(chan, peer); rc = ast_channel_make_compatible_helper(chan, peer, 0); // modify end if (rc < 0) return rc; /* Set up translation from the peer to the chan */ // modify //rc = ast_channel_make_compatible_helper(peer, chan); rc = ast_channel_make_compatible_helper(peer, chan, 1); // modify end return rc; }
ast_channel_make_compatible_helper判断呼叫双方通道的编码是否已经兼容,如果否,就调用ast_set_read_format和ast_set_write_format分别对readformat和writeformat进行设置,并建立转码路径。
/*! rief Set up translation from one channel to another */ /* modify : Add a 'bool' argument to judge which is a caller channel and which a callee channel. if 'bool' is true, then 'from' is a caller channel, 'to' is a callee channel. otherwise, 'to' is a caller channel, 'from' is a callee channel. */ static int ast_channel_make_compatible_helper(struct ast_channel *from, struct ast_channel *to, int bool) { format_t src, dst; //int use_slin; /* See if the channel driver can natively make these two channels compatible */ if (from->tech->bridge && from->tech->bridge == to->tech->bridge && !ast_channel_setoption(from, AST_OPTION_MAKE_COMPATIBLE, to, sizeof(struct ast_channel *), 0)) { return 0; } if (from->readformat == to->writeformat && from->writeformat == to->readformat) { /* Already compatible! Moving on ... */ ast_log(LOG_NOTICE, "Already compatible! "); return 0; } /* Set up translation from the 'from' channel to the 'to' channel */ src = from->nativeformats; dst = to->nativeformats; /* If there's no audio in this call, don't bother with trying to find a translation path */ if ((src & AST_FORMAT_AUDIO_MASK) == 0 || (dst & AST_FORMAT_AUDIO_MASK) == 0) return 0; if (ast_translator_best_choice(&dst, &src) < 0) { ast_log(LOG_WARNING, "No path to translate from %s to %s ", from->name, to->name); return -1; } /* if the best path is not 'pass through', then * transcoding is needed; if desired, force transcode path * to use SLINEAR between channels, but only if there is * no direct conversion available. If generic PLC is * desired, then transcoding via SLINEAR is a requirement */ // modify : comment these /* use_slin = (src == AST_FORMAT_SLINEAR || dst == AST_FORMAT_SLINEAR); if ((src != dst) && (ast_opt_generic_plc || ast_opt_transcode_via_slin) && (ast_translate_path_steps(dst, src) != 1 || use_slin)){ ast_log(LOG_NOTICE, "dst is AST_FORMAT_SLINEAR! "); dst = AST_FORMAT_SLINEAR; } */ // modify end // add /* we only build translation path and do translations in the callee channel. to achieve this goal, we set readformat and writeformat of the caller channel and the callee channel both to nativeformat of the caller channel,so the caller channel won't execute read-transcode and write-transcode. */ if(bool){ dst = src; } // add end if (ast_set_read_format(from, dst) < 0) { ast_log(LOG_WARNING, "Unable to set read format on channel %s to %s ", from->name, ast_getformatname(dst)); return -1; } if (ast_set_write_format(to, dst) < 0) { ast_log(LOG_WARNING, "Unable to set write format on channel %s to %s ", to->name, ast_getformatname(dst)); return -1; } return 0; }
在ast_set_read_format中,先调用ast_translator_best_choice(&fmt,&native)从native(即对应通道的nativeformates)和fmt(要设置的编码集合)中分别选择最优的一种编码,并将选择出的编码重新赋值给native和fmt,这里是传地址的,通过指针修改。然后把native的值赋值给rawformat(通道的rawreadformat),将fmt的值赋值给format(通道的readformat)。最后,如果format与native值不相同的话,就调用ast_translator_build_path(*format, *rawformat)来建立转码路径的链表。ast_set_write_format与ast_set_read_format同理,只不过fmt是赋值给通道的writeformat,native是赋值给通道的rawwriteformat。
收到200 OK后,会调用process_sdp(file:channels/chan_sip.c)来解析SDP,在处理被叫终端的SDP时,被叫通道的nativeformats可能会改变,此时需要重新设置被叫通道的读写转码路径,对于writetranscode: 从channel->writeformat到channel->nativeformats进行转换,对于readtranscode: 从channel->nativeformats到channel->readformat进行转换。被叫通道的writeformat和readformat在之前调用ast_channel_make_compatible时已经被设置过了。
if (!(p->owner->nativeformats & p->jointcapability) && (p->jointcapability & AST_FORMAT_AUDIO_MASK)) { if (debug) { char s1[SIPBUFSIZE], s2[SIPBUFSIZE]; ast_debug(1, "Oooh, we need to change our audio formats since our peer supports only %s and not %s ", ast_getformatname_multiple(s1, SIPBUFSIZE, p->jointcapability), ast_getformatname_multiple(s2, SIPBUFSIZE, p->owner->nativeformats)); } p->owner->nativeformats = ast_codec_choose(&p->prefs, p->jointcapability, 1) | (p->capability & vpeercapability) | (p->capability & tpeercapability); ast_set_read_format(p->owner, p->owner->readformat); ast_set_write_format(p->owner, p->owner->writeformat); }
再回到我们的问题上来。经过分析,出现该问题的原因是在收到被叫的200 OK之前channelB->nativeformats的音频编码是Ulaw,而在收到200 OK后,channelB->nativeformats的音频编码是G729,而在通道桥接时,并没有检查channelB->nativeformats是否发生变化,没有去更改channelB->writeformat(例子中为ulaw)和channelA->readformat(例子中为ulaw),使主叫仍按照之前选择的路径去进行转码,从而导致了不必要的转码步骤。
另外,在asterisk关于转码的实现中,主叫和被叫通道都分别有 WriteTranscode和ReadTranscode(可以在通话时通过core show channel sip/{EXTEN}查看),这样每一路通话最多可能会用到四个转码资源(ast_trans_pvt)。针对这些问题,我修改后的做法是:只在被叫通道一侧做转码,主叫、被叫通道的readformat与writeformat设置为主叫通道的nativeformats(主叫的nativeformats是不会改变的),这样主叫通道不需要分配转码资源,最多只占用两个转码资源。并且如果在处理完被叫终端回复的SDP后被叫通道的nativeformats改变了,不需要对主叫和被叫的readformat和writeformat重新设置,只需要重新设置被叫通道的读写转码路径即可(具体修改见以上代码中modify和add部分)。
ast_channel_make_compatible这个函数只是检查呼叫中的两个channel的数据结构的一些编码成员并设置两个channel之间的编码路径。实际的转码是桥接时(如:ast_generic_bridge)在ast_read或ast_write中进行的,在读写帧之前调用ast_translate按照设置好的转码