共计 12762 个字符,预计需要花费 32 分钟才能阅读完成。
前言
接上篇批改 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)
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 个发动呼叫的办法
@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
办法,其中 op
是SalCallOp
,在 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
信息,同时也能够缩小发送数据包的大小,加重网络压力