修改linphone-sdk-android-下篇

1,008 阅读9分钟

前言

接上篇修改linphone-sdk-android-上篇

接中篇修改linphone-sdk-android-中篇

本文是下篇,本篇记录在上篇中提到的问题1排查过程及修复方案,尽量描述排查问题过程中的思路与方向

上篇中说问题1当初认为是linphone的bug,后面看源码及查资料发现可能不是bug,本篇将记录个人的理解

问题

这里再描述下问题1:打开音频编解码G722、G729等时,发起呼叫的INVITE SDP中,没有G722、G729的rtpmap

 m=audio 7078 RTP/AVP 96 0 8 9 18 101 97
 a=fmtp:18 annexb=yes
 a=rtpmap:101 telephone-event/48000
 a=rtpmap:97 telephone-event/8000
 a=rtcp-fb:* trr-int 1000
 a=rtcp-fb:* ccm tmmbr

分析

这里先了解下SDP协议,参考The Session Description Protocol (SDP) (3cx.com)

rtpmapSession attribute lines,即为会话属性行,是对Payload Type的补充说明,Payload Type既是m=audio 7078 RTP/AVP 96 0 8 9 18 101 97AVP后面的数字,这些数字是音频编解码对应的代码,对应关系如下:

下表源自Real-Time Transport Protocol (RTP) Parameters (iana.org)

PT imgEncoding Name imgAudio/Video (A/V) imgClock Rate (Hz) imgChannels imgReference img
0PCMUA80001[RFC3551]
1Reserved
2Reserved
3GSMA80001[RFC3551]
4G723A80001[Vineet_Kumar](#)(www.iana.org/go/rfc3551)]
5DVI4A80001[RFC3551]
6DVI4A160001[RFC3551]
7LPCA80001[RFC3551]
8PCMAA80001[RFC3551]
9G722A80001[RFC3551]
10L16A441002[RFC3551]
11L16A441001[RFC3551]
12QCELPA80001[RFC3551]
13CNA80001[RFC3389]
14MPAA90000[RFC3551](#)(www.iana.org/go/rfc2250)]
15G728A80001[RFC3551]
16DVI4A110251[Joseph_Di_Pol]
17DVI4A220501[Joseph_Di_Pol]
18G729A80001[RFC3551]
19ReservedA
20UnassignedA
21UnassignedA
22UnassignedA
23UnassignedA
24UnassignedV
25CelBV90000[RFC2029]
26JPEGV90000[RFC2435]
27UnassignedV
28nvV90000[RFC3551]
29UnassignedV
30UnassignedV
31H261V90000[RFC4587]
32MPVV90000[RFC2250]
33MP2TAV90000[RFC2250]
34H263V90000[Chunrong_Zhu]
35-71Unassigned?
72-76Reserved for RTCP conflict avoidance[RFC3551]
77-95Unassigned?
96-127dynamic?[RFC3551]

从表中了解到,Payload Type(PT) code 0 - 95为静态类型,即code对应固定的codec(编解码器),96 - 127为动态codec,即需要在SDP协商过程中确定

接下来追踪下源码,看看SDP中为什么没有rtpmap

先找到Java层发起呼叫的代码,在Core.java中有4个发起呼叫的方法

 @Nullable
 Call invite(@NonNull String var1);
 ​
 @Nullable
 Call inviteAddress(@NonNull Address var1);
 ​
 @Nullable
 Call inviteAddressWithParams(@NonNull Address var1, @NonNull CallParams var2);
 ​
 @Nullable
 Call inviteWithParams(@NonNull String var1, @NonNull CallParams var2);

具体实现在CoreImpl.java中,查看这个public Call inviteAddress(@NonNull Address addr);方法吧

 private native Call inviteAddress(long nativePtr, Address addr);
 ​
 @Override @Nullable
 synchronized public Call inviteAddress(@NonNull Address addr)  {
     return (Call)inviteAddress(nativePtr, addr);
 }

Java层调用了native层代码,打开编译后生成的linphone_jni.cc,找到Java_org_linphone_core_CoreImpl_inviteAddress方法

 JNIEXPORT jobject JNICALL Java_org_linphone_core_CoreImpl_inviteAddress(JNIEnv *env, jobject thiz, jlong ptr, jobject addr) {
     LinphoneCore *cptr = (LinphoneCore*)ptr;
     if (cptr == nullptr) {
         bctbx_error("Java_org_linphone_core_CoreImpl_inviteAddress's LinphoneCore C ptr is null!");
         return 0;
     }
     
     LinphoneAddress* c_addr = nullptr;
     if (addr) c_addr = (LinphoneAddress*)GetObjectNativePtr(env, addr);
     
     jobject jni_result = (jobject)getCall(env, (LinphoneCall *)linphone_core_invite_address(cptr, c_addr), TRUE);
     return jni_result;
 }

native层调用了linphone_core_invite_address这个方法,在IDE中,可以通过Ctrl+左键点击进行跳转,linphone_core_invite_address位于linphonecore.c

 LinphoneCall * linphone_core_invite_address(LinphoneCore *lc, const LinphoneAddress *addr){
     LinphoneCall *call;
     LinphoneCallParams *p=linphone_core_create_call_params(lc, NULL);
     linphone_call_params_enable_video(p, linphone_call_params_video_enabled(p) && !!lc->video_policy.automatically_initiate);
     call=linphone_core_invite_address_with_params (lc,addr,p);
     linphone_call_params_unref(p);
     return call;
 }

linphone_core_invite_address方法中调用了linphone_core_invite_address_with_params发起呼叫,这个方法较长,删减一些不关心的代码

 LinphoneCall * linphone_core_invite_address_with_params(LinphoneCore *lc, const LinphoneAddress *addr, const LinphoneCallParams *params){
     const char *from=NULL;
     LinphoneCall *call;
     
     if (!addr) {
         ms_error("Can't invite a NULL address");
         return NULL;
     }
 ​
     parsed_url2=linphone_address_new(from);
     call=linphone_call_new_outgoing(lc,parsed_url2,addr,cp,proxy);
     
     bool defer = Call::toCpp(call)->initiateOutgoing();
     if (!defer) {
         if (Call::toCpp(call)->startInvite(nullptr) != 0) {
             /* The call has already gone to error and released state, so do not return it */
             call = nullptr;
         }
     }
 ​
     return call;
 }

linphone_core_invite_address_with_params方法中调用linphone_call_new_outgoing方法创建Call对象,调用initiateOutgoing方法初始化发起呼叫并设置当前状态为OutgoingInit,接下来调用startInvite方法发起呼叫,startInvite方法位于call.cpp中,在其中又调用getActiveSession方法获取CallSession,调用CallSession::startInvite方法

 int Call::startInvite (const Address *destination) {
     return getActiveSession()->startInvite(destination, "");
 }

CallSession::startInvite方法位于call-session.cpp中,在这个方法中找了半天,没见有与SDP发送相关的逻辑,先去头文件中看看方法原型吧

找了半天也是有点收获的,分析出调用addAdditionalLocalBody去组装自定义扩展头数据

 int CallSession::startInvite (const Address *destination, const string &subject, const Content *content) {
     L_D();
     d->subject = subject;
     /* Try to be best-effort in giving real local or routable contact address */
     d->setContactOp();
     string destinationStr;
     char *realUrl = nullptr;
     if (destination)
         destinationStr = destination->asString();
     else {
         realUrl = linphone_address_as_string(d->log->to);
         destinationStr = realUrl;
         ms_free(realUrl);
     }
     char *from = linphone_address_as_string(d->log->from);
     /* Take a ref because sal_call() may destroy the CallSession if no SIP transport is available */
     shared_ptr<CallSession> ref = getSharedFromThis();
     if (content)
         d->op->setLocalBody(*content);
 ​
     // If a custom Content has been set in the call params, create a multipart body for the INVITE
     for (auto& content : d->params->getCustomContents()) {
         d->op->addAdditionalLocalBody(content);
     }
 ​
     int result = d->op->call(from, destinationStr, subject);
     ms_free(from);
     if (result < 0) {
         if ((d->state != CallSession::State::Error) && (d->state != CallSession::State::Released)) {
             // sal_call() may invoke call_failure() and call_released() SAL callbacks synchronously,
             // in which case there is no need to perform a state change here.
             d->setState(CallSession::State::Error, "Call failed");
         }
     } else {
         linphone_call_log_set_call_id(d->log, d->op->getCallId().c_str()); /* Must be known at that time */
         d->setState(CallSession::State::OutgoingProgress, "Outgoing call in progress");
     }
     return result;
 }

CallSession::startInvite方法原型为,

 virtual int startInvite (const Address *destination, const std::string &subject = "", const Content *content = nullptr);

是个virtual虚函数,说明有函数复写,在IDE中搜索发现MediaSession类继承自CallSession,好的,找到MediaSession复写的startInvite方法,方法较长,删除一些不关心的代码

 int MediaSession::startInvite (const Address *destination, const string &subject, const Content *content) {
     L_D();
     
     // 删除不关心的代码
 ​
     d->op->setLocalMediaDescription(d->localDesc);
 ​
     int result = CallSession::startInvite(destination, subject, content);
     if (result < 0) {
         if (d->state == CallSession::State::Error)
             d->stopStreams();
         return result;
     }
     return result;
 }

MediaSession::startInvite中调用setLocalMediaDescription方法组装本地媒体描述信息,最后再调用父类的CallSession::startInvite方法继续发起呼叫,好的,现在只关心setLocalMediaDescription方法,其中opSalCallOp,在IDE中打开call-op.cpp,找到setLocalMediaDescription方法,删减一些不关心的代码

 int SalCallOp::setLocalMediaDescription (SalMediaDescription *desc) {
     if (desc) {
         sal_media_description_ref(desc);
         belle_sip_error_code error;
         belle_sdp_session_description_t *sdp = media_description_to_sdp(desc);
         vector<char> buffer = marshalMediaDescription(sdp, error);
         belle_sip_object_unref(sdp);
         if (error != BELLE_SIP_OK)
             return -1;
 ​
         mLocalBody.setContentType(ContentType::Sdp);
         mLocalBody.setBody(move(buffer));
     } else {
         mLocalBody = Content();
     }
     return 0;
 }

到这里终于发现与SDP相关的方法了media_description_to_sdp,继续查看media_description_to_sdp方法,此方法位于sal_sdp.c中,方法较长,主要是组装SDP协议数据,比如设置版本、创建源信息,创建会话等,这里删减一些不关心的代码

 belle_sdp_session_description_t * media_description_to_sdp(const SalMediaDescription *desc) {
     belle_sdp_session_description_t* session_desc=belle_sdp_session_description_new();
     bool_t inet6;
     belle_sdp_origin_t* origin;
     int i;
     char *escaped_username = belle_sip_uri_to_escaped_username(desc->username);
 ​
     if ( strchr ( desc->addr,':' ) !=NULL ) {
         inet6=1;
     } else inet6=0;
     belle_sdp_session_description_set_version ( session_desc,belle_sdp_version_create ( 0 ) );
 ​
     origin = belle_sdp_origin_create ( escaped_username
                                       ,desc->session_id
                                       ,desc->session_ver
                                       ,"IN"
                                       , inet6 ? "IP6" :"IP4"
                                       ,desc->addr );
     bctbx_free(escaped_username);
 ​
     belle_sdp_session_description_set_origin ( session_desc,origin );
 ​
     belle_sdp_session_description_set_session_name ( session_desc,
         belle_sdp_session_name_create ( desc->name[0]!='\0' ? desc->name : "Talk" ) );
 ​
     // 删减不关心的代码
 ​
     for ( i=0; i<desc->nb_streams; i++ ) {
         stream_description_to_sdp(session_desc, desc, &desc->streams[i]);
     }
     return session_desc;
 }

分析media_description_to_sdp方法找到在stream_description_to_sdp方法中组装数据流信息到SDP协议中,stream_description_to_sdp方法非常长,此方法主要是组装SDP协议中编解码相关的信息,这里删除大部分不关心的代码

 static void stream_description_to_sdp ( belle_sdp_session_description_t *session_desc, const SalMediaDescription *md, const SalStreamDescription *stream ) {
     
     // 删减不关心的代码
 ​
     media_desc = belle_sdp_media_description_create ( sal_stream_description_get_type_as_string(stream)
                  ,stream->rtp_port
                  ,1
                  ,sal_media_proto_to_string ( stream->proto )
                  ,NULL );
     // 看到payloads字段
     if (stream->payloads) {
         for ( pt_it=stream->payloads; pt_it!=NULL; pt_it=pt_it->next ) {
             pt= ( PayloadType* ) pt_it->data;
             mime_param= belle_sdp_mime_parameter_create ( pt->mime_type
                     , payload_type_get_number ( pt )
                     , pt->clock_rate
                     , pt->channels>0 ? pt->channels : -1 );
             belle_sdp_mime_parameter_set_parameters ( mime_param,pt->recv_fmtp );
             if ( stream->ptime>0 ) {
                 belle_sdp_mime_parameter_set_ptime ( mime_param,stream->ptime );
             }
             // 锁定此方法
             belle_sdp_media_description_append_values_from_mime_parameter ( media_desc,mime_param );
             belle_sip_object_unref ( mime_param );
         }
     } else {
         /* to comply with SDP we cannot have an empty payload type number list */
         /* as it happens only when mline is declined with a zero port, it does not matter to put whatever codec*/
         belle_sip_list_t* format = belle_sip_list_append(NULL,0);
         belle_sdp_media_set_media_formats(belle_sdp_media_description_get_media(media_desc),format);
     }
 ​
     // 组装自定义sdp属性
     if (stream->custom_sdp_attributes) {
         belle_sdp_session_description_t *custom_desc = (belle_sdp_session_description_t *)stream->custom_sdp_attributes;
         belle_sip_list_t *l = belle_sdp_session_description_get_attributes(custom_desc);
         belle_sip_list_t *elem;
         for (elem = l; elem != NULL; elem = elem->next) {
             belle_sdp_media_description_add_attribute(media_desc, (belle_sdp_attribute_t *)elem->data);
         }
     }
     
     // 删减不关心的代码
 }

stream_description_to_sdp方法中看到payload字段,马上就要找到了happy~

经过分析,锁定belle_sdp_media_description_append_values_from_mime_parameter方法,分析此方法,在其中找到组装rtpmap的源码

 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) {
     
 #ifndef BELLE_SDP_FORCE_RTP_MAP /* defined to for RTP map even for static codec*/
     if (!mime_parameter_is_static(mime_parameter)) {
         /*dynamic payload*/
 #endif
         if (belle_sdp_mime_parameter_get_channel_count(mime_parameter)>1) {
             snprintf(atribute_value,MAX_FMTP_LENGTH,"%i %s/%i/%i"
                     ,belle_sdp_mime_parameter_get_media_format(mime_parameter)
                     ,belle_sdp_mime_parameter_get_type(mime_parameter)
                     ,belle_sdp_mime_parameter_get_rate(mime_parameter)
                     ,belle_sdp_mime_parameter_get_channel_count(mime_parameter));
         } else {
             snprintf(atribute_value,MAX_FMTP_LENGTH,"%i %s/%i"
                     ,belle_sdp_mime_parameter_get_media_format(mime_parameter)
                     ,belle_sdp_mime_parameter_get_type(mime_parameter)
                     ,belle_sdp_mime_parameter_get_rate(mime_parameter));
         }
         belle_sdp_media_description_set_attribute_value(media_description,"rtpmap",atribute_value);
 #ifndef BELLE_SDP_FORCE_RTP_MAP
     }
 #endif
     
     // always include fmtp parameters if available
     if (belle_sdp_mime_parameter_get_parameters(mime_parameter)) {
         snprintf(atribute_value,MAX_FMTP_LENGTH,"%i %s"
                 ,belle_sdp_mime_parameter_get_media_format(mime_parameter)
                 ,belle_sdp_mime_parameter_get_parameters(mime_parameter));
         belle_sdp_media_description_set_attribute_value(media_description,"fmtp",atribute_value);
     }
 }

这里先分析下mime_parameter_is_static方法是干什么的?查看以下源码发现,噢~~,原来是用于判断编解码是否是静态类型(前面提到的Payload Type)

 const struct static_payload static_payload_list [] ={
     /*audio*/
     {0,1,"PCMU",8000},
     {3,1,"GSM",8000},
     {4,1,"G723",8000},
     {5,1,"DVI4",8000},
     {6,1,"DVI4",16000},
     {7,1,"LPC",8000},
     {8,1,"PCMA",8000},
     {9,1,"G722",8000},
     {10,2,"L16",44100},
     {11,1,"L16",44100},
     {12,1,"QCELP",8000},
     {13,1,"CN",8000},
     {14,1,"MPA",90000},
     {15,1,"G728",8000},
     {16,1,"DVI4",11025},
     {17,1,"DVI4",22050},
     {18,1,"G729",8000},
     /*video*/
     {25,0,"CelB",90000},
     {26,0,"JPEG",90000},
     {28,0,"nv",90000},
     {31,0,"H261",90000},
     {32,0,"MPV",90000},
     {33,0,"MP2T",90000},
     {34,0,"H263",90000}
 };
 ​
 static int mime_parameter_is_static(const belle_sdp_mime_parameter_t *param){
     const struct static_payload* iterator;
     size_t i;
 ​
     for (iterator = static_payload_list,i=0;i<payload_list_elements;i++,iterator++) {
         if (iterator->number == param->media_format &&
             strcasecmp(iterator->type,param->type)==0 &&
             iterator->channel_count==param->channel_count &&
             iterator->rate==param->rate ) {
             return TRUE;
         }
     }
     return FALSE;
 }

现在再来分析下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中发现

 option(ENABLE_RTP_MAP_ALWAYS_IN_SDP "Always include rtpmap in SDP." OFF)
 ​
 if(ENABLE_RTP_MAP_ALWAYS_IN_SDP) 
     set(BELLE_SDP_FORCE_RTP_MAP 1)
 endif()

bingo~,真的是到最后了

最后在编译时增加编译配置项

 $ cd linphone-sdk/build/
 $ cmake -DENABLE_RTP_MAP_ALWAYS_IN_SDP=ON ..
 $ cmake --build . --parallel 8

重新编译后拷贝到AS中运行,发起呼叫查看Logcat输出

总结

在源码中看到通过BELLE_SDP_FORCE_RTP_MAP这个宏控制是否在SDP中包含静态类型编解码的rtpmap信息,个人猜测是静态类型的编解码信息,是协议中固定的,任何遵循协议的实现方,都可以根据静态类型编解码对应的code解析出相应的rtpmap信息,所以在SDP中去掉静态类型编解码器的rtpmap信息,同时也可以减少发送数据包的大小,减轻网络压力