修改linphone-sdk-android-下篇
前言
本文是下篇,本篇记录在上篇中提到的问题1排查过程及修复方案,尽量描述排查问题过程中的思路与方向
上篇中说问题1当初认为是linphone的bug,后面看源码及查资料发现可能不是bug,本篇将记录个人的理解
问题
这里再描述下问题1:打开音频编解码G722、G729等时,发起呼叫的INVITE SDP中,没有G722、G729的rtpmap
1 | m=audio 7078 RTP/AVP 96 0 8 9 18 101 97 |
分析
这里先了解下SDP协议,参考The Session Description Protocol (SDP) (3cx.com)
rtpmap是Session attribute lines,即为会话属性行,是对Payload Type的补充说明,Payload Type既是m=audio 7078 RTP/AVP 96 0 8 9 18 101 97中AVP后面的数字,这些数字是音频编解码对应的代码,对应关系如下:
下表源自Real-Time Transport Protocol (RTP) Parameters (iana.org)
PT ![]() |
Encoding Name ![]() |
Audio/Video (A/V) ![]() |
Clock Rate (Hz) ![]() |
Channels ![]() |
Reference ![]() |
|---|---|---|---|---|---|
| 0 | PCMU | A | 8000 | 1 | [RFC3551] |
| 1 | Reserved | ||||
| 2 | Reserved | ||||
| 3 | GSM | A | 8000 | 1 | [RFC3551] |
| 4 | G723 | A | 8000 | 1 | [Vineet_Kumar][RFC3551] |
| 5 | DVI4 | A | 8000 | 1 | [RFC3551] |
| 6 | DVI4 | A | 16000 | 1 | [RFC3551] |
| 7 | LPC | A | 8000 | 1 | [RFC3551] |
| 8 | PCMA | A | 8000 | 1 | [RFC3551] |
| 9 | G722 | A | 8000 | 1 | [RFC3551] |
| 10 | L16 | A | 44100 | 2 | [RFC3551] |
| 11 | L16 | A | 44100 | 1 | [RFC3551] |
| 12 | QCELP | A | 8000 | 1 | [RFC3551] |
| 13 | CN | A | 8000 | 1 | [RFC3389] |
| 14 | MPA | A | 90000 | [RFC3551][RFC2250] | |
| 15 | G728 | A | 8000 | 1 | [RFC3551] |
| 16 | DVI4 | A | 11025 | 1 | [Joseph_Di_Pol] |
| 17 | DVI4 | A | 22050 | 1 | [Joseph_Di_Pol] |
| 18 | G729 | A | 8000 | 1 | [RFC3551] |
| 19 | Reserved | A | |||
| 20 | Unassigned | A | |||
| 21 | Unassigned | A | |||
| 22 | Unassigned | A | |||
| 23 | Unassigned | A | |||
| 24 | Unassigned | V | |||
| 25 | CelB | V | 90000 | [RFC2029] | |
| 26 | JPEG | V | 90000 | [RFC2435] | |
| 27 | Unassigned | V | |||
| 28 | nv | V | 90000 | [RFC3551] | |
| 29 | Unassigned | V | |||
| 30 | Unassigned | V | |||
| 31 | H261 | V | 90000 | [RFC4587] | |
| 32 | MPV | V | 90000 | [RFC2250] | |
| 33 | MP2T | AV | 90000 | [RFC2250] | |
| 34 | H263 | V | 90000 | [Chunrong_Zhu] | |
| 35-71 | Unassigned | ? | |||
| 72-76 | Reserved for RTCP conflict avoidance | [RFC3551] | |||
| 77-95 | Unassigned | ? | |||
| 96-127 | dynamic | ? | [RFC3551] |
从表中了解到,Payload Type(PT) code 0 - 95为静态类型,即code对应固定的codec(编解码器),96 - 127为动态codec,即需要在SDP协商过程中确定
接下来追踪下源码,看看SDP中为什么没有rtpmap
先找到Java层发起呼叫的代码,在Core.java中有4个发起呼叫的方法
1 |
|
具体实现在CoreImpl.java中,查看这个public Call inviteAddress(@NonNull Address addr);方法吧
1 | private native Call inviteAddress(long nativePtr, Address addr); |
Java层调用了native层代码,打开编译后生成的linphone_jni.cc,找到Java_org_linphone_core_CoreImpl_inviteAddress方法
1 | JNIEXPORT jobject JNICALL Java_org_linphone_core_CoreImpl_inviteAddress(JNIEnv *env, jobject thiz, jlong ptr, jobject addr) { |
在native层调用了linphone_core_invite_address这个方法,在IDE中,可以通过Ctrl+左键点击进行跳转,linphone_core_invite_address位于linphonecore.c中
1 | LinphoneCall * linphone_core_invite_address(LinphoneCore *lc, const LinphoneAddress *addr){ |
在linphone_core_invite_address方法中调用了linphone_core_invite_address_with_params发起呼叫,这个方法较长,删减一些不关心的代码
1 | LinphoneCall * linphone_core_invite_address_with_params(LinphoneCore *lc, const LinphoneAddress *addr, const LinphoneCallParams *params){ |
在linphone_core_invite_address_with_params方法中调用linphone_call_new_outgoing方法创建Call对象,调用initiateOutgoing方法初始化发起呼叫并设置当前状态为OutgoingInit,接下来调用startInvite方法发起呼叫,startInvite方法位于call.cpp中,在其中又调用getActiveSession方法获取CallSession,调用CallSession::startInvite方法
1 | int Call::startInvite (const Address *destination) { |
CallSession::startInvite方法位于call-session.cpp中,在这个方法中找了半天,没见有与SDP发送相关的逻辑,先去头文件中看看方法原型吧
找了半天也是有点收获的,分析出调用addAdditionalLocalBody去组装自定义扩展头数据
1 | int CallSession::startInvite (const Address *destination, const string &subject, const Content *content) { |
CallSession::startInvite方法原型为,
1 | virtual int startInvite (const Address *destination, const std::string &subject = "", const Content *content = nullptr); |
是个virtual虚函数,说明有函数复写,在IDE中搜索发现MediaSession类继承自CallSession,好的,找到MediaSession复写的startInvite方法,方法较长,删除一些不关心的代码
1 | int MediaSession::startInvite (const Address *destination, const string &subject, const Content *content) { |
在MediaSession::startInvite中调用setLocalMediaDescription方法组装本地媒体描述信息,最后再调用父类的CallSession::startInvite方法继续发起呼叫,好的,现在只关心setLocalMediaDescription方法,其中op是SalCallOp,在IDE中打开call-op.cpp,找到setLocalMediaDescription方法,删减一些不关心的代码
1 | int SalCallOp::setLocalMediaDescription (SalMediaDescription *desc) { |
到这里终于发现与SDP相关的方法了media_description_to_sdp,继续查看media_description_to_sdp方法,此方法位于sal_sdp.c中,方法较长,主要是组装SDP协议数据,比如设置版本、创建源信息,创建会话等,这里删减一些不关心的代码
1 | belle_sdp_session_description_t * media_description_to_sdp(const SalMediaDescription *desc) { |
分析media_description_to_sdp方法找到在stream_description_to_sdp方法中组装数据流信息到SDP协议中,stream_description_to_sdp方法非常长,此方法主要是组装SDP协议中编解码相关的信息,这里删除大部分不关心的代码
1 | static void stream_description_to_sdp ( belle_sdp_session_description_t *session_desc, const SalMediaDescription *md, const SalStreamDescription *stream ) { |
在stream_description_to_sdp方法中看到payload字段,马上就要找到了happy~
经过分析,锁定belle_sdp_media_description_append_values_from_mime_parameter方法,分析此方法,在其中找到组装rtpmap的源码
1 | void belle_sdp_media_description_append_values_from_mime_parameter(belle_sdp_media_description_t* media_description, const belle_sdp_mime_parameter_t* mime_parameter) { |
这里先分析下mime_parameter_is_static方法是干什么的?查看以下源码发现,噢~~,原来是用于判断编解码是否是静态类型(前面提到的Payload Type)
1 | const struct static_payload static_payload_list [] ={ |
现在再来分析下belle_sdp_media_description_append_values_from_mime_parameter方法的意思,大意如下:如果没有定义BELLE_SDP_FORCE_RTP_MAP这个宏就执行if (!mime_parameter_is_static(mime_parameter))判断编解码是否是静态类型,如果定义了就不判断是否是静态类型
总结一下就是如果没有定义BELLE_SDP_FORCE_RTP_MAP这个宏,就不组装静态类型编解码的rtpmap信息,只组装动态类型编解码的rtpmap信息,终于找到源头了,真是拨云见日呀
到这里还没完,既然是根据宏定义做的判断,肯定在编译的时候可以配置,先看看能不能找到定义宏的地方,在IDE中全局搜索,在belle-sip下的CMakeList.txt中发现
1 | option(ENABLE_RTP_MAP_ALWAYS_IN_SDP "Always include rtpmap in SDP." OFF) |
bingo~,真的是到最后了
最后在编译时增加编译配置项
1 | cd linphone-sdk/build/ |
重新编译后拷贝到AS中运行,发起呼叫查看Logcat输出
总结
在源码中看到通过BELLE_SDP_FORCE_RTP_MAP这个宏控制是否在SDP中包含静态类型编解码的rtpmap信息,个人猜测是静态类型的编解码信息,是协议中固定的,任何遵循协议的实现方,都可以根据静态类型编解码对应的code解析出相应的rtpmap信息,所以在SDP中去掉静态类型编解码器的rtpmap信息,同时也可以减少发送数据包的大小,减轻网络压力
