linphone 架构及组成模块 2

linphone 系统框图 3

linphone 中各个模块说明 3

linphone 中数据结构说明 7

linphone 的初始化过程 7

linphone 建立通话过程说明 10

1 拨号call过程 10

2 等待响应 16

3 Answer过程分析 21

4 关于RTP及音视频流的网络传输 22

5 总结 23

linphone 会话执行过程log分析 24

linphone 使用参考 40

基于linphone-3.3.2版本,新版本linphone-3.4.3支持同时有多路call,所以,相比之前版本会有不少变化。

一 linphone 架构及组成模块

Linphone是一款跨平台的可视电话客户端软件,同时支持视频通话功能。Linphone可以在Linux,windows等主流操作系统平台上运行。

Linphone基于开源软件构建,本身也是开源软件。Linphone架构中sip协议的处理基于osip以及exosip两个开源库实现,媒体数据的选择整合处理使用mediastream2完成,该软件使用ffmepg、speedx等多款开源软件完成音视频的编解码,并通过ortp完成基于rtp协议的音视频数据传输。ortp是一款处理RTP会话的开源软件。

1 整体架构图如下:

整个软件分为两层,上层为用户接口前端(user interface frontends),下层为linphone核心引擎(linphone core engine)。

2 功能模块说明:

Liblinphone 核心引擎实现了linphone所有的功能函数,而且能够方便的添加音频和视频的呼叫功能。Liblinphone也提供高层的API,用来初始化,接收或者终止呼叫。Liblinphone依赖于下面三个组件:

1 Mediastreamer2

这是一个支持多种平台的轻量级的流技术引擎,主要适合于开发语音和视频电话应用程序。该引擎主要为 linphone 的多媒体流的收发,包括语音和视频的捕获、编码解码以及渲染。

2 ortp2

Ortp是一个RTP库。为基于RTP协议的媒体流传输提供支持。通过mediastream2编码的数据就是使用ortp库发送到网络的另一端。

3 eXosip2

Exosip2为sip协议的实现。这部分实际上是由exosip2和osip2两个库共同完成的。使用sip协议完成路由、媒体协商以及会话的建立和管理,为直接的媒体流的传输提供基础。

二 linphone 系统框图

关于上面框图的一些说明:

通话双方在通信前使用exosip进行会话协商。上图左边部分展示这一部分的流程。Exosip后台任务完成数据的接收和发送,并通过事件队列通知linphone底层的状态变化。

filter的构建在会话协商成功建立后就顺带完成了,并且ticker任务也跑起来了。此时按照filter graphics构建的通道,音视频流不断的从硬件设备上读取,并经过编码压缩送给RTP会话,之后送到对端,对端到达的音视频流也经过RTP会话接收送到解码解压缩filter,还原出原始的音视频流交给硬件设备播放。媒体数据在这两路流中源源不断的流动,完成了双方的可视通话。

上层linphone的core任务也不断的对底层进行迭代检查。所做的基本工作如下:

对于sip协议部分,core一直等待从事件队列上拿事件。这些事件是exosip任务在处理sip消息过程中添加到事件队列上的。每当得到新的事件后,core就从应用层的角度出发,进行处理。

对于视频流:基本上只处理rtcp数据包到达的事件。stream上也有一个事件队列,用于保存该流上的相关事件。对于rtcp数据包事件,core也只处理sr类型rtcp包,即发送端报告,得到jitter 和包丢失率。如果设置了自适应比特率,则调用相关接口进行处理。此过程不断进行,直到当前事件上的包处理完。

对于音频流,检查流是否还是活动的。通过比较RTP stats中接收的数据包数目是否发生变化,如果在超时时间到达后,接收的数据量还没有发生变化,则认为音频没有响应。

三 linphone 中各个模块说明

1 Linphone coreapi中子模块说明:

Coreapi中的各个模块就是上层的处理模块,包括configure文件的处理接口,address的处理接口,chat的处理接口,sal的处理接口,proxy的处理接口,authorization的处理接口,friends的处理接口,callback的处理接口,state的处理接口,杂项处理接口等。这相当于高层的几个模块,提供给用户的接口调用主要都在linphonecore.c中

1.Callback模块:

该模块下的回调函数都是用于sal模块调用的。当sal处理完sip协议的处理后,就会调用相应的callback函数继续后续的处理,包括启动一个音视频传输流,启动响铃等。也就是说这里的callback完成了media媒体层的处理以及linphone上层的处理。

回调函数被保存在全局变量linphone_sal_callbacks中,在linphone初始化时调用sal_set_callbacks设置到sal的callback上去的。

2.Genera_stat模块:

主要提供linphone全局状态的修改与设置的接口

3.Address模块:

调用sal提供的接口,进行与地址相关的处理,这里的地址主要是uri相关的处理。包括获取地址以及地址中的部分信息或者设置这些信息。在上层地址是一个字符串指针,但是在内部处理时都会强制转换为osip_from结构体来处理。实际上就是对linphone_address结构体的处理。

4.Authorization模块:

处理认证信息。各个认证用户的信息都被保存到linphone_auto结构体中兵串接在linphone_core结构体上。这里的接口就是处理这些数据结构,提供设置和获取相关信息的接口。

5.Chat模块:

提供创建和销毁chat room,向chat room发送消息和从chat room接收消息的接口,以及设置和获取用户数据的接口。类似于authorization模块,所有的chat room信息也是保存在linphone_chat结构体中并串接在linphone_core结构体上的。

6.Friends模块:

提供处理friends相关信息的接口。所有的friends信息保存在linphone_friend结构体中并被串接在linphone_core结构体上,这样操作起来,包括设置,获取,添加以及移除都很方便。

7.Configure模块:

提供配置文件处理的相关接口,包括配置文件的解析,配置文件中信息的获取,写入,同步等。配置文件解析后便于程序处理的信息主要都保存在lpconfig结构体中,这与文本文件中便于编写和阅读的配置文件本身不同。

配置文件中的各个配置模块本身也按照section的方式进行了划分,各个section也都是挂接在lp_config的section链表上的。这个模块可以单独提取出来进行测试。

8.Offer_answer模块:

管理基于sdp的媒体协商。根据本地的支持能力和远端支持的能力,根据就低的原则,获得双方都可以支持的媒体信息。比如编解码格式等。

9.Presence模块:

提供与在线状态相关的处理。

10.Proxy模块:

处理代理相关的处理。代理相关的信息保存在linphone_proxy结构体上,但是该结构体只是代表了当前linphone_core使用的proxy。代理可能不止一个,所有的代理其信息都被串接在链表上,并被挂接在sip_conf的proxies上。添加一个代理,取得一个代理以及其他相关的操作接口也都在该模块中提供处理接口。

11.Sal模块:

Sal模块其实应该是最重要的,最核心的模块了。该模块对exosip进行了简单的封装,间接的对osip模块进行了封装,使用该模块的接口可以完成sip协议的处理以及媒体描述的处理。

Sal.c文件主要是对一些sal相关的结构体的操作,包括SalMediaDescription和sal_op。处理包括创建这些结构体的实例,获取或者设置其中的一些操作域。

Sal_exosip2_sdp.c基于osip库提供的sdp相关操作的接口,在sal层实现将其与sal相关的结构体关联起来操作。比如根据SalMediaDescription结构体信息将其转换为sdp结构体,或者反之。

Sal_exosip2_presence.c包括了对in和out的subscribe的操作。Text数据的发送(基于osip和exosip)。

Sal_exosip2.c sip这块比较重要的封装。包含了对sal_op结构体的创建和基本操作。对exosip重要结构体的封装,包括初始化和释放。包含了对sal结构体的创建和基本操作的封装,更重要的是包含了对sal和sal_op,sal_media_desc,sal_stream_desc这些上层结构体与底层osip_message,sip_message,sdp_message等数据结构之间数据的转换和共享,以及对底层相关接口的调用。这种调用主要包括跟据上层结构体中包含的信息设置底层结构体,并调用底层接口完成具体功能,以及根据底层结构体得到的数据设置上层结构体的相关信息。

一个基本的描述就是:sal作为signal abstract layer包含了上层所主要理解的交互信息,这些信息对于理解电话操作而言已经足够了,在底层,选择了osip和exosip来支持这项操作。所以实际上来说,可以用其他支持sip的库的接口来替代现有的,保留sal层接口的功能定义。在linphone中,虽然大部分使用了sal层的封装来完成sip交互过程,但是也调用osip和exosip库本身的其他接口,所以这层封装主要还是再次简化协议层的处理,使得功能更具体,而不是更单一。

几个关键数据结构之间的关系:

Sal一个基本的结构体,通过这个结构体可以搜寻到上层所需的所有sip协议相关的信息。具体的call,register等信息保存在sal_op这个结构体中,多个实体通过链表串接起来,挂在sal上。Sal_op包含了sal_op_base结构体,这个结构体保存了一些通用的不变的信息,对多个实体而言,比如路由信息,本地媒体信息,远端媒体信息等。其root指针由返回指向了sal这个基础,所以通过sal_op可以找到sal。另外,在媒体信息中包含了所有流的信息,所有这些类似一个树的组织结构,sal类似树根,通过它可以找到所有的枝叶及其上的信息。这些数据结构之间的关系如下图:

12.Core模块:

上层API及其封装实现。通过这些API接口,可以快速构建基于sip的可视电话系统。

2 底层模块说明

1.Mediastream实现的说明:

从代码上来看mediastreamer库,它的构成非常结构化。在mediastream2中实现了大量的filter,包括声卡视频卡的filter,编解码器的filter,RTP传输与接收的filter等等。每个.c文件实现一个filter,而且每个filter的实现也是非常结构化的。首先需要定义一个filter结构体实例,对于实例的各个部分进行赋值,主要是包括定义filter私有数据,filter的methods,init,preprocess,process,postprocess和uninit这几个函数的实现等。而关键的一些实现,比如编解码器的处理,是基于ffmpeg库提供的接口来完成的,而声卡视频卡摄像头数据的捕获由其他库实现,但是也是基于标准的驱动接口来完成的。对于RTP的接收和传输则是基于ortp库来完成的。

另外提供了两个文件,audiostream和vediostream,用来处理音视频流的filter连接,linphonecore主要就是调用这几个文件提供的接口来完成媒体流的启动的。

其他的就是辅助数据结构,包括filter的定义注册,queue队列的操作,声卡和摄像头的handler的操作,ticker的处理等。

考虑到设备描述符,实际上mediastream可以分为两部分来看,一部分就是之前主要的有关大量filter的操作定义部分,一部分就是后来的有关声卡设备和摄像头设备的设备描述符的定义和操作。

关于摄像头部分,视频数据的捕获是通过创建一个新的任务来进行的,该任务源源不断的从设备上读取数据,当然除此之外也提供了配置设备的参数的接口,比如图形的大小格式等。而且对于video模块,设备处理和读取数据是与V4lState这个结构体挂钩的,这个结构体实例又挂到filter的私有数据上,这样数据读取任务不断的从驱动读取数据,放到该结构体实例上,filter处理时从自己的私有数据结构指向的queue中取数据自然的就取到了摄像头捕获的数据,实际上这里包含了设备的操作(基于设备描述符)和filter的操作(基于filter的描述符)以及二者的结合。

在filter中webcam设备可能不止一个,有关这些设备的操作辅助接口就在webcam.c中实现。

之前说了摄像头部分,对于声卡部分也是极其类似的,管理辅助函数在mssndcard文件中实现,另外还配以几个声卡设备本身的描述符文件来描述,其与filter的关系类同于视频部分。还有就是这些设备的描述符结构体汇总在mscommon中,而filter的汇总在alldescs.h头文件中。

2.ortp

关于ortp模块的说明参见参考资料1

四 linphone 中数据结构说明

程序中定义了一个比较大的数据结构体——linphonecore,将其作为总的控制结构。通过该结构体,可以找到所需的大部分信息。这些信息要么是直接在该结构体中定义,要么是在其包含的模块相关的子结构体中。可以想象,内存中保存的该结构体,就像整体软件信息的总控点,通过该总控点,可以直接或者间接的得到系统相关的信息,这在一定程度上可以简化系统的架构和实现。(在许多开源软件中都可以看到这种代码架构方式。)下图展示了linphonecore结构体:

指针sal指向sal数据结构图,通过该指针,就可以找到sal模块用到的所有数据结构体。

Config部分指向linphone的配置信息,包括网络,sip协议,rtp传输,音视频参数以及编解码器信息。

call指针挂载了所有的通话,每一路通话都由linphonecall结构体表示。

Audiostream和videostream保存了媒体信息,a_rtp和a_rtcp保存了RTP相关的信息。

五 linphone 的初始化过程

首先初始化全局的状态:power为GSTATE_POWER_OFF,即关闭状态;reg为GSTATE_REG_NONE,没有注册;call为GSTATE_CALL_IDLE,空闲状态。

设置新的power状态为startup。

1.Ortp库初始化,调用ortp_init

在ortp_init中主要创建了payload type链表,所有支持的payload type都被创建在一起了,这里注册的payloadtype就是底层RTP传输所能够支持的。

添加payload type,先为音频,后为视频。

2.调用ms_init初始化mediastream库,该库封装了媒体处理接口,使得多媒体数据的处理变得简单。

Ms_init中形成了三个全局链表,一个为filter描述符链表,所有media streamer支持的filter的描述符在这里被串接在一起了,包括编解码,音视频读写以及RTP发送和接收处理相关的filter。第二个为音频卡描述符链表,所有支持的音频卡相关的描述符也被串接在一起。第三个就是视频卡描述符链表,处理类似音频卡描述符。

3.调用sal_init进行sip协议栈的初始化。该过程将返回一个sal结构体。

Exosip全局结构体的创建以及初始化。

需要注意,在这里相当于有三层封装调用:一层为sal层的封装调用,一层为exosip层的封装调用,最底层为osip层的基本调用。

另外需要注意的是在这里没有创建exosip任务,而是在后面的读取并配置sip配置信息时才创建exosip任务,并监听特定端口。

将lc->sal的up指针指回linphone core全局结构体

设置sal上的回调函数,这些回调函数在对应的sip协议处理完后用于调用来处理外层有关call与media流的一些处理。

如果配置文件中没有设置sip会话的过期时间,则在这时将其设置为200

将所有sip setup配置串联到registered_sip_setups全局链表上

4.读取配置文件中有关音频的设置并对声卡进行设置

根据配置文件中描述的声卡设备id将声卡设备配置到linphone core结构体的sound配置结构体对应声卡的描述项上。

设置用于响铃的音频文件的路径。该文件路径会被保存到sound configure结构体的local_ring项目上。

类似上面,设置用于提示远端响铃的音频文件的路径,最终保存到sound configure结构体的remote ring项目上。

检查系统中的声卡设备

配置是否使能回声消除

配置回声限制

配置增益

配置回放增益

5.读取配置文件中有关网络部分的配置并对网络参量进行配置

读取配置文件中有关带宽的设置,并对linphone中音视频使用的带宽进行配置。首先保存带宽值到net config结构体中,音频带宽配置为配置文件中读出的值与默认值的小者,视频带宽配置为读出值减去音频值减去10和0之间的较大者。这里10相当于是一个缓冲。

设置stun server

设置nat防火墙地址

配置是否使用防火墙策略

配置是否只对SDP进行nat转换

配置mtu值

设置信息分包时间

6.读取配置文件中有关RTP部分的配置并对该模块进行配置

首先设置音视频的RTP端口号

配置RTP音视频抖动补偿时间,默认为60毫秒

配置nortp超时时间,即没有RTP或者rtcp数据包时linphone认为对端crash或者网络中断的超时值

设置在静音时不进行RTP传送,默认为FALSE

7.读取配置文件中有关编解码器相关的信息并设置到linphone的编码器信息结构体上

读取配置文件中的音频编解码器信息,如果有,则查找之前初始化ms模块时建立的全局过滤器描述符信息表,如果找到,则说明相应的编解码器支持,否则不支持配置文件中添加的编解码器。对于视频也类似。

通过上述操作,所有的配置文件中可支持的编解码器信息都被创建到audio_codecs和video_codecs两个链表上了,之后将它们添加到linphone的codecs_conf结构体上。

另外,在这步操作时,也将系统支持的其他编解码器添加到了codes_conf结构体上,基本原理如下:对于video,查找所有的RTP初始化时创建的payloadtype表,如果是video类型的payload,并且被mediastreamer支持,但是不在配置文件描述中,就将其添加到链表上。也就是系统初始化时支持的但是在配置文件中没有说明的编解码器也会被添加到配置结构上。

更新已分配的音频带宽值。找到需要最大带宽的音频编解码器,将其带宽需求除以1000作为linphone的audio_bw值,同时重新设置linphone网络配置中的上下行带宽值。

Desc_list上的编解码器信息与RTP初始化时设置的payload type信息的区别:

Desc_list将medie streamer支持的所有filter的信息都串联在一个链表上,不仅包括了编码器解码器,还包括了RTP发送和接收filter,以及音视频的读写filter。这些都是站在media streamer这个中间层的角度来考虑。Filter的构成也很规范,包括了一些必要的描述信息以及数据的读写处理接口。

Payload type链表是所有RTP传输中支持的payload的链表,每一个type的描述信息主要描述了该类型的type的时钟速率,正常比特率,采样比特等,可以看出这些信息与传输也是紧密相关的。除此之外,也描述了编码信息,这部分信息与desc_list中的编码器信息部分会存在交集。

8.读取配置文件中有关sip协议的相关信息,并以此来配置linphone的sip模块。

配置是否在发送数字时使用sip info信息。

配置是否在发送数字时使用rfc2833信息

配置是否使用ipv6

配置sip的传输端口信息。指定是使用随机值还是知名5060端口

将端口信息设置到linphone core中,并启动sip监听。这样,当sip协议数据到达时即可被处理。

首先调用sal_listen_port启动监听端口。在这里,协议层被选择和设置,一般情况下都是udp,这里为eXtl_udp。之后创建并启动_eXosip_thread任务,该任务处理sip协议数据的接收,协议的处理,状态机的处理,数据的发送等。即几乎所有与sip协议有关的处理都会在该任务中处理完。最后保存用户代理信息。

获取配置文件中的联系人信息,如果联系人为空,或者配置文件中联系人信息不为空,但在将其设置为主联系人信息时出错(比如格式错误),则基于环境变量中的host和user信息创建主联系人,否则将配置文件中的联系人信息设置到sip_conf结构体的联系人上

如果配置文件中设置了猜测主机名,则将该配置设置到linphone core的sipconfigure结构体上

配置incoming call的超时时间,如果超过超时时间没有answer则terminate该call

读取并配置代理信息,所有的代理者信息都被添加到sip configure的代理者链表上

读取并配置默认代理者信息。默认代理者会从所有代理者中挑选,根据配置文件,然后放到linphone core结构体的default_proxy上

读取并配置认证信息。首先从配置文件中读取usrname,userid,password,ha1,realm等信息,并根据这些信息创建一个新的认证信息结构体,将其添加到linphone core的auth_info链表上。同时,查找所有处于pending状态的待认证事件,如果linphone core中能找到一致的认证信息结构体,则对其进行认证。

根据配置文件对sip_conf的其他变量进行设置

9.读取配置文件中有关video模块的配置并将其设置到linphone core中

首先读取所有的video设备,将其添加到video_conf的cams项上

读取配置文件中video部分有关device的设置,在系统中查找是否已经有该设备。如果没有找到,则使用default设备配置。如果原来保存的设备不为空,并且与新设备不一样,则基于新设备重新触发preview预览。如果linphone已经准备好了,并且video_conf上的设备不为空,则将该设备的描述串写入配置文件,也即覆盖之前的配置。另外,如果如果描述串中有static picture则描述串在被重新写到配置文件之前会设置为空。

根据配置文件设置视频size

根据配置文件设置其他配置项,包括是否进行capture,是否display,是否self_view等。

10.设置linphone之前和当前的模式都为在线状态。设置最大call logs为15

11.读取并配置ui

读取配置文件friends  section部分的信息,创建friends结构体并保存配置。将所有的friends添加到friends链表上。并额外做一些其他处理。

最后读取call logs 信息,并创建call logs结构体保存logs信息,将所有logs添加到call logs链表上。

12.显示ready。

全局状态配置为power on

将sip_conf中的auto_net_state_mon设置到linphone core上

Linphone core的Ready设置为TRUE

至此,整个初始化过程完成。初始化后的内存数据结构体及状态:

首先需要创建linphone顶层最全局结构体linphonecore也即程序中多处用到的lc。对于phone的很多相关操作,该对象是主要的handler,在整个程序运行过程中在内存中只有一个实例存在。

初始化ortp库,加载支持的音视频RTP payload类型到全局结构体av_profile上。同时全局结构体linphone_payload_types也指向这些payload元素列表。

初始化ms库。Desc_list全局指针挂载了所有支持的filter描述符信息。加载所支持的声卡设备的描述符到全局变量scm上,加载支持的所有视频捕获设备的描述符到视频全局变量scm上。

初始化sal模块,在此过程中初始化exosip库,在exosip初始化过程中初始化了osip库。在此过程中,从下到上,osip全局状态机被加载,osip全局结构体对象也被创建,其上的事务链表,callback等子项也被设置,exosip全局结构体对象也被创建,其上底层协议处理部分以及内存分配部分也部分的被初始化,osip对象被挂载到了exosip对象上。Sal结构体对象被分配内存。

Linphone core被挂载到sal上了,sal的回调处理函数被加载。

配置文件中的配置项被一步步的加载,同时linphone的配置部分也被不断的在内存中创建出。状态被更新,整个初始化过程也就此完成。

六 linphone 建立通话过程说明

1 拨号call过程

用户执行呼叫,调用call命令

Linphone中调用该命令对应的执行函数lpc_cmd_call

如果lc-call不为空,也就是说linphone全局结构体上当前的会话还存在,则输出打印,要求用户先关闭当前call;否则调用linphone_care_invite处理call命令。(该版本只支持一个call存在)

在linphone_core_invite中:

首先调用linphone_core_interpret_usl解析URL地址。输入的地址可能是一串字符,通过该函数将其中的关键元素解析出来,主要是按照osip_from结构体的格式解析出来。

然后调用linphone_core_invite_address进一步的以osip_from格式的地址为参数进行处理,完了释放地址参数。

在linphone_core_invite_address中:

在该函数中对地址信息进行进一步的简单处理,获取到对外的本地信息,然后调用linphone_call_new_outgoing发起会话请求。

linphone_call_new_outgoing中,

首先创建linphone call内存对象实例。方向设置为call outgoing,创建一个salop对象实例,其root指针指向初始化时创建的全局sal结构体上,并对op的一些基本域进行设置。在会话过程中,与sal相关的操作主要还是基于salop的。另外,userpointer指针指向call本身。

调用linphone_core_get_local_ip获取本地的ip地址。如果设置了nat防火墙策略,则就用nat防火墙地址为本地ip地址,否则,如果配置了ipv6,则使用ipv6地址。如果仍然没有得到结果,则调用sal_get_default_local_ip接口来获取。该接口使用底层exosip提供的方法,即利用socket的gethostbyname接口来在与网关建立connected连接后从socket中获取本地ip地址信息。

调用create_local_media_description创建本地媒体描述结构体。在该接口中,创建一个SalMediaDescription对象实例,并用之前获取的信息对其进行设置,包括流数量为1,本地ip地址,用户名,带宽。针对其携带的流,流的地址为本地ip地址,流的端口为配置的RTP音频端口,proto为SalProtoRtpAvp,类型为audio,ptime为netconfig中的down_ptime。对于payload的配置逻辑如下:从编解码器配置项中读取所有的音频编解码器payload,如果是enabled,并且linphone当前支持该编解码器,带宽也允许,则将该payload添加到streamer的payload链表上。目前payload的带宽是通过payload的Bitrate计算出来的。计算方法:包大小为ip4头+UDP头+RTP头+Bitrate除以50*8,即除以400。因为Bitrate为基于比特单位,除以8变为字节,但是这里除以50是为什么?貌似是包的数量,也就是一秒钟采样的数据用50个包来传送。此时计算出来的为包的大小,将其再乘以8乘以50变为包含ip、UDP及RTP包头的情况下的Bitrate,此值除以1000作为linphonecore中的音频带宽值。基于上述计算得出来的音频带宽值和网络配置中配置的上下行带宽值更新linphonecore的带宽配置的方法如下:如果给的带宽值为0,即表示无穷,音视频的下载带宽设置为-1,否则,计算出来的音频带宽值和给定带宽值中较小者作为音频下载带宽,给定带宽值与音频下载带宽的差值减去10与0的较大者作为视频下载带宽值,也就是视频带宽或者为0,或者为可用带宽减去音频所用的带宽(优先保证音频)再减去10作为缓冲界限后的值。上传带宽的配置方法也一致。如果当前配置的带宽值中的较小者(上行和下行选择)大于当前编解码器Bitrate所占用的带宽,则将其clone一份放到streamer的payload链表上,否则不添加。在处理完配置的音频编解码器payload后,从全局av_profile中找到telephone-event也将其添加到streamer的payload链表上。Streamer的带宽值被设置为音频下载带宽值,应该小于等于media的带宽配置。如果视频也被允许,那么streamer1将作为视频流的描述符,同样从RTP配置信息中得到视频端口赋给该描述符,proto同音频部分,类型此时为SalVideo。类似音频部分,将codec配置中的符合带宽限制的视频编解码器payload添加到该streamer的payload链表上。如果视频下载带宽不为0,则该streamer的带宽值被设置为视频下载带宽值。至此,media描述符就创建完成。Media描述符当前是被挂载到call的localdesc上。

调用linphone_call_init_common对call的其他域进行设置,状态为LCStateInit,start_time为当前时间,media_start_time为0,创建call_log实例记录拨号记录,通知所有的friends我们当前的onthephone状态。如果是设置了stun服务器,则调用linphone_core_run_stun_tests测试stun服务器,并配置streamer的endpoint candidate的端口和地址。

调用discover_mtu获取当前的mtu值,这只在当前netconfig中的mtu设置为0时才进行。发现mtu的过程也是通过向对端发送数据,然后根据socket的options操作来查看,比如根据收到的ICMP包信息,根据获取到的mtu值重新设置mediastream的mtu。

综上,在new outgoing的过程中,我们创建了call实例,salop实例,media实例同时包含streamer实例。基本关系为call-->salop,call-->media-->stream。本质上来讲还是在初始化call,但在此过程中也初始化了需要的salop,以及media。

上一步通过linphone_call_new_outgoing为发起一个新的会话做好了准备,包括创建了需要的call实例,salop实例等,这些相关信息保存到call对象中,该对象在此时被挂载到linphonecore上。

如果目的代理不为空,或者sipconfi的ping_with_options为FALSE,则掉用linphone_core_start_invite发起会话请求,否则,call的状态被设置为LCStatePreEstablishing,该状态指示稍后,即ping完后继续发起invite请求。为了完成ping操作,先创建一个用于ping的salop,调用sal_ping基于该op以及from和real_url参数发送sip的ping数据,重新将call的start_time设置为当前时间。

对于linphone_core_start_invite调用:

在调用该接口前,我们已经创建了linphone core 实例,并在发起invite请求的准备过程中创建了call实例,以及得到了dest_proxy地址信息。在这些准备工作完成的前提下,系统进一步处理invite相关的后续操作:

首先调用get_fixed_contact来获取联系人信息。

如果当前设置了防火墙,并配置了nat地址,则从linphone core结构体实例中获取首要的contact信息。这些信息基本上是从sip_conf中拿取的。

如果上一步失败,并且call上的salop结构体实例已经被创建了,并且其上的contact信息不为空,则返回空,表明不需要修改contact信息。

如果上一步失败,则判断ping_op操作是否成功完成,如果是,则使用ping_op上的contact信息。

如果上一步失败,并使用了代理,则使用register时的contact信息

如果还失败,则使用本地ip地址和配置到linphone core上的端口信息组合出contact信息返回

如果在上一步获取sip_conf中的contact信息时返回失败,则该接口此时返回空

通过上面调用,如果获取到了contact信息,则将其设置到call 的op的contact域上。

Call的state设置为LCStateInit

调用linphone_core_init_media_streams初始化媒体流。参数为linphone core实例和call实例

首先从call实例的localdesc上拿到media描述符

基于media描述符上的stream[0]也就是音频流的端口调用audio_stream_new创建audiostream。

在audio_stream_new中,创建了一个audiostream结构体实例,stream的session域被初始化为一个RTP session,这是通过调用create_duplex_rtpsession创建的。在该接口中,创建了一个全双工的RTP session结构体实例。首先通过调用rtp_session_new为RTP session实例分配内存,并调用rtp_session_init对这个session进行初始化。这里的初始化包括设置session的mode(send  or recv or send_and_recv)。并根据mode设置session的flags。如果可以发送,发送ssrc初始化为一个随机值。并设置默认的源描述信息for rtcp,挂在session的sd上。设置session的rcv和snd的profile,此处都设置为av_profile全局变量了。初始化rtp和rtcp的socket都为-1,配置默认的接收和发送socket  buffer大小。

从rtp_session_init中出来接着配置RTP session的一些类似全局的参数。Recv_buff_size配置为MAX_RTP_SIZE,调度模式为关闭状态,阻塞模式为不使用该模式,自适应平衡抖动补偿,对称RTP,设置本地地址,此时会创建rtp和rtcpsocket,并对socket的参数进行配置。比如是否reuse address,设置socket buffer size等。注册timestamp和ssrc_changed事件的回调函数,以及ssrc changed的触发阈。最后返回创建的RTP session 结构体实例。

返回的RTP session 实例被挂到了stream的session域上。接着调用ms_filter_new创建了RTP send filter结构体实例。这被挂载到了stream的RTP send域上了。最后对流的相关参数进行了初始化。在ms_filter_new中,会遍历系统最初初始化时创建的filter 描述符链表desc_list,从其上找到id与当前要创建的id一致的描述符,然后调用ms_filter_new_from_desc基于该描述符创建filter。在这个接口中,首先初始化了一个msfilter的结构体实例,然后把当前找到的描述符挂到该filter的desc域上,调用描述符的init接口对filter进行初始化,最后返回这个filter。

Audiostream创建成功后被挂载到了linphone core结构体实例的audiostream域上。

基于初始化linphone core  config时,从配置文件及系统中获取的对sound部分的配置信息对audiostream进行实际使用上的配置。也就是针对具体使用实例的配置。之前可能就是全局的系统参数级别的配置。这包括增益的配置,回音消除的配置,echo limiter的配置,自动增益的配置,噪音的相关配置等。

如果在linphone core初始化时已经初始化了rtp_transport,则将audiostream上的RTP session上的rtp和rtcp上的tr指向linphone core的 a_rtp和a_rtcp上。相应的,这两个结构体的session指针指向这里的session。

至此,音频流的初始化工作基本完成。

如果系统定义了视频流的支持,则开始进行视频流的初始化。这是通过video_stream_new接口完成的,参数类似音频部分,只不过这里是用的media的stream[1]描述符上保存的视频RTP端口。

在video_stream_new中,创建了一个videostream结构体实例。类似与音频部分,通过调用create_duplex_rtpsession接口创建一个全双工的视频RTP会话,并将其挂载到videostream的session域上。

在create_duplex_rtpsession接口中,创建全双工的RTP session 结构体实例。并作初始化工作,这步同音频部分。

调用ortp_ev_queue_new创建一个ortp event queue,事件队列。

调用ms_filter_new创建一个RTP send filter,在该接口中会创建一个msfilter实例。初始化工作同音频部分。

调用rtp_session_register_event_queue同时将流的事件队列注册到RTP会话上。也就是session的eventqs也指向stream的evs。

设置视频的高度和宽度,返回视频流videostream实例。

从上面的初始化工作中可以看出,音频流和视频流的RTP session和filter的创建是一致的,调用相同的接口完成,因此可以看出是通用的,只是相应的挂载到了音频流和视频流上。至此基于call的音视频流的初始化工作就完成了。

如果在sip_conf中没有设置sdp_200_ack,则将call的media_pending设置为TRUE。另外,将call的localdesc描述符设置给call的op,这是通过sal_call_set_local_media_description接口来完成的。在该接口中,首先增加localdesc的引用计数,同时递减op上的localdesc的引用计数,如果减一后为零,则释放其资源,同时将参数中给出的localdesc给该op。

从call的log实例上获取from和to地址,分别作为发起call会话的from和正式url即real_url。至此,基本的初始化的工作做得差不多了,调用sal_call发起sip协议请求。Sip的Invite消息报文在此时才真正的发给对端。

在sal_call中:

会话操作基于call的op实例发起。从参数中获取from和to地址,将其设置到op的from和to域中,并调用sal_exosip_fix_route来检查路由。

在sal_exosip_fix_route接口中,首先判断op的route是否为空,如果是空,就不做任何处理,说明没有配置路由,否则,继续。

调用osip_route_init分配并初始化一个osip_from结构体实例。然后调用osip_route_parse将op中的字符串形式的route解析为这里osip_from格式的结构体,如果失败,说明route配置有误,将op中的route重新该为null,否则从uri列表中查找是否有名为lr的节点,如果没有则添加一个,值为null,并将此修改更新到op的route项中。最后释放操作过程中为临时变量分配的内存。

Route检查了,现在调用eXosip_call_build_initial_invite接口创建一个invite消息。在该接口中,我们首先将to参数指定的字符串形式的目的地址解析到osip_from格式的结构体中,如果解析失败,返回。接着调用generating_request_out_out_dialog创建一个最小的out_of_dialog的request请求消息体。

在generating_request_out_out_dialog中,此时我们已经操作到sip底层协议部分了,主要是调用exosip和osip库来进行相关操作。这里我们首先检查exosip全局变量下的extl是否为空,也就是底层网络协议的支持,默认是UDP。如果这块为空,说明底层协议支持没有完成,后续将不能收发数据包,直接返回错误。接着根据extl中proto_family指定的协议簇通过socket接口来猜测主机的ip地址,如果失败则返回错误。接着调用osip_message_init分配并初始化一个osip_message结构体实例。到时候sip数据包中按照协议规定的格式的各个位置的数据会被解析到osip_message结构体中,进而在程序中传递和处理。接着准备invite类型sip包的请求行,包括method,invite,协议2.0,status_code为0,reason_phrase为null。如果method为register,则调用osip_uri_init为request分配并初始化uri结构体实例,并将proxy参数中的信息解析到该结构体中。基本按照同样的方法将from参数的数据解析到request的to的uri中。

对于非register类型的请求,操作则如下:此时将to参数解析并设置到request的to的uri中,如果成功,并且其uri不为空,则将uri上headersl链上的元素一个一个取出来。如果这个原始不是from,to,call-id,creq,via,contact则将其拷贝给request。这步首先调用osip_header_init分配并初始化一个osip_header的结构体实例,然后将之前取出来的header的name和value拷贝给这个结构体,最后将其挂载到request的headers的链表上,也就是osip结构体的headers的链表上。无论是不是上述拷贝的header,最后都从原来的链表上移除并释放内存。也就是从最初的从to参数解析到osip_from结构体实例上的。对于非register请求,如果存在代理,调用osip_route_init分配并初始化一个osip_from结构体实例,将proxy参数数据解析到该结构体中,类似之前,从该结构体中查找名称为lr的url_params,将osip上的to上的url拷贝到req_uri中,并将解析后的proxy添加到osip的routes上。否则,就将proxy上的url给osip的req_uri。释放proxy本身的内存。此时调用osip_message_set_route为osip的router分配内存并将to参数的数据解析赋值给它。如果不存在代理,则直接将osip的to的url给osip的req_uri。

至此,register和非register类型的请求的不同处理完成了。调用osip_message_set_from将参数from解析并配置到osip结构体中。调用osip_from_set_tag将新生产的一个随机数作为from的tag。分配并初始化osip_call_id类型的结构体实例,生成随机数配置其number,并将其挂到request的call_id上。分配并初始化osip_creq类型的结构体实例,如果是register请求,则其number设置为1否则为20,然后也将其挂载到osip的cseq上。

调用_eXosip_request_add_via配置request的via。

设置max-forward为70。如果method为options,则配置request的accept为“application/sdp”。用eXosip全局变量上的user_agent配置request请求中的user_agent域。最后返回osip的request消息结构体。

调用_eXosip_dialog_add_contact添加联系人信息,最后挂载到osip的contact域上。

调用osip_message_set_subject添加subject信息,这个是sip头的subject信息。

最后调用osip_message_set_expires设置sip头的expires为120秒。意思就是说如果经过120秒对端还不响应就取消当前的invite请求的发送。

在通过上述操作创建osip基本消息结构体实例后,在外层继续对其进行一些初始化。这包括设置allow,即允许的options方法。如果op的base中包含了contact信息,则将初始化osip结构体时添加的contact信息清除,重新将这里的contact信息设置进去,因为初始化时这些都是根据系统信息猜测的,在没有进行任何配置的情况下使用。如果op中session_expires不为零,则将invite的头部添加session_expires,值为200。并添加timer的support。如果op中本地的媒体信息也已经被配置了,则将op的sdp_offering设置为TRUE,将本地媒体信息的配置通过调用接口set_sdp_from_desc设置到请求osip结构体中。在上述接口中首先将媒体描述信息转换为sdp_message格式的结构体,在sdp_message中间格式的sdp数据转化为串行的字符串,最后将其挂载到osip结构体的bodies域上。

至此完成了基本的设置,调用eXosip_lock()和eXosip_unlock()保护eXosip底层的发送数据函数,这里为eXosip_call_send_initial_invite。最后保存call的id到op的cid上,并调用sal_add_call将当前op挂载到根sal的calls链表上。所有创建成功的op都会被挂载到sal的calls上,程序通过sal实例就可以遍历所有的与sip协议处理有关的op。

现在看看在接口eXosip_call_send_initial_invite中做了哪些操作:首先调用eXosip_call_init初始化一个exosip_call结构体实例,并为其分配内存。接着调用exosip_transaction_init初始化一个osip_transaction结构体实例。事务的初始化信息大部分都是来自之前初始化的osip_message类型的request结构体的。初始化完成之后就将transaction挂载到全局osip结构体的对应类型事务的链表上,同时,也将其挂载到exosip_call的out事务域上。根据request信息,new一个out的sipevent。这时osip_event类型的,不同于exosip_event。为了关联具体的事物,events的事务ID设置为之前创建的事务。设置事务的your_instance域,这个域用来保存一些有用的东西,这里使用jinfo_t类型的结构体将exosip_dialog,exosip_call,exosip_subscribe以及exosip_notify关联起来,交给事务的your_instance。不过此时还只有exosip_call的信息,其他的实例还没有创建。下一步将之前创建的sip_event挂载到事务上,并将创建的exosip_call实例挂载到exosip的j_calls域上。调用exosip_update更新exosip实例,调用exosip_wakeup唤醒exosip任务的处理。最后返回刚创建的call的id。因为我们在exosip的osip的事务队列上添加了一个事件,之后exosip任务会处理该事件,并根据事件描述发送sip的invite消息。

此时再判断sip的配置中sdp_200_ack是否设置了。和之前不同,如果设置了,则将call的media_pending设置为TRUE,并同意将call中的local media 描述设置到call的op上。感觉在sal_call中并没有改变call的media description。调用linphonecore的vtable函数显示正在连接远端客户端。

如果之前调用sal_call时返回失败,则显示无法建立call提示,并stop媒体流,这是linphonecore上的媒体流,同时清除call上有关RTP profile的描述。最后调用linphone_call_destroy销毁之前创建的call。其实这里可以看出通过顶层的call可以找到底层创建的所有实例。

如果之前调用成功了,则修改linphonecore上的状态为call_out_invite。最后返回。至此,顶层的call命令处理完了。

从上面的发起会话请求的初始化过程来看,系统基本上是沿着从外层到内层,从上层到下层的次序来初始化的,外到内是说,先是call,在到call上的op,再到op上的media desc,再到其上的stream desc。这几步初始化了对媒体流的控制结构体,因为主要都是desc即描述信息。接着道stream本身,到其上的RTP session,到filter,基本上就与流本身的传输相关了。从上到下是说,先初始化上层协议相关的结构体,比如sip协议相关的数据结构的初始化,接着是媒体和RTP等下层相关协议的初始化。

2 等待响应

此时invite请求包发送给对端了,客户端的相关状态也设置好了,就等待对端的响应并进行处理。

之前已经说明,call调用是在main里面处理的,处理完成后调用相关接口将请求和数据交给exosip任务去实际的发送数据。之后main仍然每间隔一秒进行迭代操作,而exosip任务则处理数据包的收发和相关底层的超时操作。因为此时call的状态为init,这样在linphone core迭代中不会去处理call相关的操作。

当对端有响应之后,该消息最先被exosip任务捕获到。Exosip在调用exosip_read_message时会从UDP套接字接口上读取到对端的响应数据包。接着调用_exosip_handle_incoming_message对缓冲在缓冲区的数据进行处理。

在该接口中,首先调用osip_parse解析缓冲区中的sip信息。在osip_parse中,创建一个osip_event类型的结构体实例,将buf中的sip数据解析到该结构体的sip域中,根据消息中的类型设置events的类型。对于输入的message,有如下类型的定义,invite请求,ack请求,除此之外的请求,以及1开头、2开头以及3456开头的状态响应。之后将该events返回。

调用osip_message_fix_last_via_header根据host和port信息检查sip中的via域。

调用osip_find_transaction_and_add_event查找该响应对应的事务,并将由此生出的events添加到事务的事件队列上。在该接口及子接口的调用中,首先根据响应消息的类型找到osip全局变量上其所属的transactons队列,再到队列中查找所有的事务,找到与当前给的events匹配的事务。这有点类似于二级过滤。如果找到了,就将刚才创建的events添加到该事务的事件队列上。

如果事件添加成功,则返回,否则,说明事件没有对应的事务,继续处理。如果消息是request消息,调用exosip_process_newrequest处理,否则调用exosip_process_response_out_of_transaction处理。在newrequest中,会创建一个新的transaction,将事件挂到该事务上,并将事务挂接到osip全局变量对应的事务链表上。之后针对事件的类型,进行一些对应的处理,比如发送响应等。否则,将事务加载到exosip全局变量的事务队列上,返回。对于response,说明收到了out_of_transactions的响应,在exosip上进行查找做一些处理。

对响应消息处理完后,或者是对消息直接做了响应,或者将需要处理的放到了事务队列的某个事务的事件队列上。接着,exosip迭代检查处理事务队列上的所有事务。在处理过程中,如果需要传给上层处理,就会构造一个exosip_event结构体实例,添加到exosip的事件队列上,由linphone_core任务也就是main主线程去处理。

假设此时底层进行了协议的协商处理,基本顺序为send(invite)-->recv(180ring)-->recv(200 ok)-->send(ack)会话建立。当处理到第三步,也就是主机准备走第四步时,对端的200 ack我们收到了,此时我们再发送自己的ack之前需要处理对端的媒体类型,并为媒体流做好进一步的准备。此时callback的处理顺序为首先进入cb_rcv2xx回调,在该回调处理中调用cb_rcv2xxx_4invite回调接口。在该接口中最终会调用report_call_event将exosip_call_answered类型的事件交给linphone_core任务。

Linphone_core任务在sal_iterate处理中处理底层协议交上来的事件时,如果判断为上述事件就调用call_accepted接口。在call_accepted中:

首先调用find_op从sal全局结构体的call上查找call_id等于events的cid的op,如果找到就返回。然后将op的did赋值为events的did。调用exosip_get_sdp_info从对端最后一次的with session description的200ack中取出有关sdp的信息。实际上在该接口中会将字符串形式的从对端收到的sdp信息解析到sdp_message结构体中并返回。如果获取成功,则根据该sdp创建主机端对远端客户的媒体描述,基本步骤如下:

调用sal_media_description_new创建远端的media_desc结构体实例,将其添加到op的base的remote_media上。调用sdp_to_media_description将之前解析出来的sdp信息配置到media_desc上。如果配置成功,则调用sdp_process处理音视频流处理。

在sdp_processs中,创建一个新的media_desc结构体实例,放到op的results上,如果op的sdp_offering被设置,则调用offer_answer_intiate_outgoing接口创建一个基于本地的offers和远端响应的能力的流。否则,调用offer_answer_initiate_incoming接口创建一个流基于本地的能力和远端的offers。这个流会作为一个answer发送给远端的offers。对于outgoing,会遍历本地offers的所有streamer,取出其protocol和type,然后在远端answer中查找,如果找到,就使用这两个stream_desc调用initiate_outgoing设置results的流描述。在该接口中,取出二者的payloads的交集,端口,addr,带宽和ptime使用remote的answer,最后media描述的addr也设置为remote的answer的值。对于incoming,遍历远端的offers的所有streamer,取出其proto和type,在本地能力中找到匹配项。类似的,在incoming_initiate中,取出二者payloads的交集,而此时端口,adr,带宽,ptime等则使用本地的。这几步实际上是找出共同支持的媒体描述,交给results保存。另外,如果是基于远端offers来适配的话,将根据results构造转换出sdp_message结构体的sdp_answer给op,这在后续会被用来反馈给对端,表明我们支持的能力。

Sdp_process处理完后,调用exosip_call_build_ack将协议规范中需要的ack发送给对端。在该接口中首先调用exosip_call_dialog_find在exosip的jcall上查找同样dialog_id的call及其上的dialog。如果没有找到,则返回错误,没有call对应,否则继续调用exosip_find_last_invite基于之前找到的exosip_dialog在其上再查找携带invite消息类型的事务。这里会将incoming和outgoing类型的事务都找一下,如果只存在一个,则取其之,否则,比较两个的birth_time,取离当前时间点更接近的一个。找到会话后,调用exosip_build_request_within_dialog构造需要回复的osip_message类型的ack结构体实例及其部分内容。初始化的部分信息直接从dialog上获取,因为它保存了部分有关会话的关键信息。另一部分信息是从事务上来获取的,调用exosip_call_reuse_contact,从事务的orig_request上确定osip_message类型的信息,将其拷贝到这里创建的osip_message类型的ack上。之后再对其进行一些其他方面的配置。这样,build ack的工作就完成了。

在call_accepted里对ack还有一些其他设置,包括将op上的contact信息设置到这里的ack上,另外,如果op的sdp_answer存在,调用set_sdp将其配置到ack上,实际就是配置到其sdp message body上。最后调用exosip_call_send_ack将该ack发送给对端。与之前有所不同,这里是直接调用底层发送接口cb_snd_message将响应送给对端,而不是作为一个事务添加到事务队列上再交给exosip任务去处理。

之后调用sal上注册的回调函数call_accepted来基于op进行处理。

在回调函数call_accepted中,首先判断call的状态,如果是lcstateavrunning,则说明是已经accepted了,直接返回就可以了,否则继续处理。

如果linphone_core上audiostream的ticker不为空,说明在其他地方之前已经启动媒体传输了,先调用linphone_core_stop_media_streams停掉媒体流,再调用linphone_core_init_media_streams重新创建一个媒体流。

如果call的resultdesc不为空,则先释放它,通过递减引用计数来做到。之后将之前协商的存在于op上的resultdesc拷贝给这里的call。之前是通过localmedia和remotemedia来协商最终二者都能兼容的媒体类型的。如果拷贝成功,则增加该resultdesc的引用计数,并将call的media_pending设置为FALSE。

如果resultdesc存在了,并且其上的流描述信息不为空,则将linphonecore的state设置为gstate_call_out_connected,然后调用linphone_connect_incoming继续处理来打开媒体流的传输。

在linphone_connect_incoming接口中,首先显示connected状态,然后将call的状态设置为lcstateavrunning,如果linphonecore的ringstream不为空,则先调用ring_stop停止流,然后调用linphone_core_start_media_streams开启流媒体的传输。

在start_media_streams接口中,首先将rtp的jitter补偿设置为声卡延迟和rtp中有关audio的jitter配置二者大的一方。设置call的media_start_time。

首先处理音频部分,调用sal_media_description_find_stream从call的resultdesc中找proto是salprotortpavp类型是salaudio的stream,实际上是stream描述符。如找到了并且端口不为0,则继续处理,否则表明没有响应媒体的定义,出错返回。有说明在本地支持前提下,在和对端协商结果下,可以进行音频流的处理和传输。此时,调用make_profile基于媒体描述和流的描述创建会话需要的rtpprofile。在该接口中先调用rtp_profile_new创建一个rtpprofile类型的结构体,名称为call profile,遍历之前找到的straem流的描述符,取出其上支持的所有的payloadtype,将他们添加到刚创建的call profile中。另外,还有其他一些操作,包括保存了第一个payload的序号,根据带宽的调整,配置了相应的payloadtype。通过该接口创建的rtpprofile被赋值给call供当前会话使用。之前在初始化时已经创建了全部profile,av_profile,这是在RTP初始化过程中进行的,而这里创建的应该是该profile的一个子集,针对其上的payloadtype来说。如果linphonecore的use_files没有被设置,则取出音频播放设备和捕获设备的描述信息,调用接口audio_stream_start_now将这两个描述符作为参数传递进去启动音频传输。

audio_stream_start_now接口进一步调用audio_stream_start_full进行处理。这几个接口是mediastream库提供的。在start_full中,第一步调用rtp_session_set_profile将profile设置到流的rtpsession上。如果参数远程端口大于零,则调用rtp_session_set_remote_addr_full将远程ip地址,远程端口以及远程rtcp端口号设置到session上。这里不仅将这些参数设置到rtpsession上的相关域保存起来了,而且也调用了socket的connect与远端同时建立连接了。之后会将已建立连接的标识rtp_socket_connected设置到session的flags上。接着将payload号和jitter补偿分别设置到rtpsession上。

如果远端端口号大于零,调用ms_filter_call_method调用filter的methods,id为ms_rtp_send_set_session。在这里,filter下面有filter-desc,filter_desc上面有methods,可以用来对filter进行配置。Ms_rtp_send_set_session这个id将调用函数sender_set_session这个接口,在该接口中根据rtpsession中的send上的profile和payload number得到payload type。如果payload type的mime_type为g722,senderdata数据结构体上的rate设置为8000,否则根据payload type的clock_rate来设置。最后将senderdata的session指向rtpsession。Senderdata结构体实例是send filter的data指针指向其私有的数据,包含了与RTP发送有关的信息。对此,其他filter也类似,也就是说每个filter都有其私有的数据,用于其在process中使用,比如recordfile对应的filter的私有数据为文件指针,也就是保存数据的文件指针,这样就很好理解了。

调用ms_filter_new创建一个新的filter,根据id ms_rtp_recv_id从desc_list上取得id一致的filter描述符,然后基于该描述符创建新的filter,并将filter的desc指针指向该描述符。接着类似发送,调用filter的methods,id为ms_rtp_recv_set_session配置rtpsesson的recvsession。该id会调用接口receiver_set_session。在该接口中的操作同样类似于sender中的。

调用ms_filter_new创建dtmf的filter放到stream的dtmfgen上。

调用rtp_session_signal_connect接口注册回调函数,如果有电话音,telephone events调用on_dtmf_received,payload type changed调用payload_type_changed接口。

接着配置音频读取和播放卡设备。如果captcard不为空,则基于该描述符调用ms_snd_card_create_reader创建音频数据读取的filter。这是通过该描述符的create_reader函数完成的。如果为空,则调用ms_filter_new创建一个ms_file_player_id的filter,交给audio_stream的soundread。另外,创建一个ms_resample_id的filter给audio_stream的read_resampler。

如果infile不为空,调用audio_stream_play基于stream播放该文件。

如果playcard不空,则调用ms_snd_card_create_writer基于该card创建soundwrite,及音频数据的发送filter。否则,则调用ms_filter_new创建ms_file_rec_id的filter,并在outfile参数不为空的情况下调用audio_stream_record记录接收的音频数据保存到outfile中。

到这里声卡设备的filter都已经创建了。接着创建编码和解码器的filter。首先调用rtp_profile_get_payload从profile和payload参数中取出payload type。接着调用ms_filter_create_encoder和ms_filter_create_decoder接口基于payload type的mime_type创建解码器和编码器的filter,并保存到stream的encoder和decoder两个filter域上。实际上在这个接口中,都是根据mime_type查找desc_list全局链表,找到匹配的desc,再创建新的filter结构体实例并把desc放到filter上。

如果支持回身消除,则创建ms_speex_ec_id的filter放到streame上,调用ms_filter_set_sample_rate对应的methods设置采样码率。如果stream上ec_tail_len,ec_delay,ec_framesize等不为空,就调用filter的对应的方法将这些配置值设置到filter上去。这些值可能是在初始化时或者协商媒体时得到的,当时只有转而配置到当前用的filter上才能具体的在本次通话过程中其作用。
   如果stream的el_type不等于elinactive或者use_gc或者use_ng被置位了,则分别为volsend和vlorecv创建两个filter,这可能是用来远程传输音量信息的。如果el_type不等于ELInactive,则说明?调用filter的MS_VOLUME_SET_PEER对应的methods将volsend和volrecv联系起来。接着判断如果等于ELControlFull则调用filter methods配置volrecv的MS_VOLUME_ENABLE_NOISE_GATE。如果use_ng被设置了,调用methods将volsend的filter的MS_VOLUME_ENABLE_NOISE_GATE使能。

如果use_agc被配置了,调用volsend该filter的MS_VOLUME_ENABLE_AGC对应的methods使能agc

根据之前获得的payload type的clockrate信息调用soundread和soundwrite filter的MS_FILTER_SET_SAMPLE_RATE方法将这些配置信息设置到filter中。另外,如果read_resampler和write_resampler如果为空,则调用ms_filter_new基于MS_RESAMPLE_ID创建两个filter。

基于MS_FILTER_SET_NCHANNELS方法设置soundwirte该filter。

基于payload type中clockrate的配置来设置encoder编码器filter的采样率,通过MS_FILTER_SET_SAMPLE_RATE对应的methods。用normal_bitrate配置通过MS_FILTER_SET_BITRATE配置编码器filter的Bitrate。

基于clock_rate配置解码器filter的采样率,id为MS_FILTER_SET_SAMPLE_RATE的方法完成。

如果send_fmtp不为空,将其添加到编码器filter上,如果recv_fmtp不为空,将其添加到解码器filter上。

调用ms_filter_new基于MS_EQUALIZER_ID创建平衡filter,放到stream的equalizer上。并通过MS_EQUALIZER_SET_ACTIVE方法将eq_active设置到该filter上。

如果read_resampler和write_resampler不为空,调用audio_stream_configure_resampler用声卡filter和RTPfilter来配置均衡filter。该接口将声卡或者RTP的采样率取出来,配置到resample中。对于read_resample,使用soundread配置from的rate,rtpsend配置to的rate,对于writer_resample,rtprecv和soundwrite分别为from和to的rate。

下一步将所有的filter连接起来。通过调用ms_connection_helper_start和ms_connection_helper_link两个接口完成。

对于sending graph,为:

Soundread-->read_resampler-->ec-->volsend-->encoder-->rtpsend

对于recv graph,为:

Rtprecv-->decoder-->dtmfgen-->equalizer-->volrecv-->ec-->write_resampler-->soundwrite

连接后的数据结构体间的关系如下图所示,以sending为例

除了filter连接在一起外,还需要一个发动机来推动数据在所有filter间流动,之前是建好了通路,现在需要一个泵来推数据在这个通路中流动。在代码中就是需要创建一个ticker。这时通过ms_ticker_new接口来完成的,挂载在stream上。通过该接口也同时创建了ticker需要的thread,并启动了该thread。接着调用ms_ticker_attach将soundread和rtprecv两个filter绑定到ticker上。在该接口中,我们首先遍历filter链上的所有filter,调用preprocess接口,即将所有预先要处理的操作通过preprocess接口来完成,这在构造filter时需要注意。接着将要绑定的filter连到ticker的execution_list上。实际上后续ticker任务在跑动的时候,就是遍历execution_list链表,从各个graph的filter头开始将链上的各个filter的process接口调用执行一遍,这样反复循环,数据就从一个filter传输到下一个filter了。

至此audio_stream_start_full接口处理就完成了。

Ticker任务运行的函数为ms_ticker_run,在该接口中就是调用run_graphs接口将graph的通路走一遍的。在run_graphs中,遍历ticker的execution_list列表,对其上的filter头执行run_graph。在run_graph接口中,如果filter的last_ticks不等于ticker的ticks才执行filter的处理。该ticker值可以用来表示该filter是否是刚处理过的filter,避免接连的处理。如果需要处理,首先调用filter_can_process查看该filter之前是否有一个已经在运行了。这一步是通过如下方式判断的:遍历filter的所有input对应的msqueue,如果该queue的prev指针对应的filter的last_tick不等于ticker上的ticks,则返回失败,说明该filter之前的filter还在处理中,所以当前filter不可处理,也就是说该filter的所有input还没有完全被其之前的filter填充完,还有input没有接收完其前面filter的数据,因此将其放到未调用filter链表上。否则返回成功,处理该filter。

此时将ticker的ticks赋值给filter的last_tick,后续判断二者相等即可表明该filter已经被处理了。接着调用call_process处理。在该接口中,首先判断如果该filter的desc指示的filter的ninputs为零,表明该filter只有一个输入,或者desc指示的标识flags为MS_FILTER_IS_PUMP,则调用ms_filter_process直接处理。否则,检查filter的所有input端口对应的queue,如果有数据就调用ms_filter_process处理。至于将多个input端口的处理分开来,主要是考虑到在一次调用中需要将filter的所有input处理完,因此如果有多个input,接口将多次调用ms_filter_process直到每个input都被处理了。

在ms_filter_process中,调用filter的desc的process接口函数指针进行处理。实际上每个filter的核心处理在创建该filter时,其desc已经全都准备好处理接口了。

退出call_process退回到run_graph中,处理完当前filter后,对于filter的所有output对应的queue,对其next指针指向的filter继续调用run_graph接口进行处理。实际上就是对当前filter之后的所有filter调用run_graph继续处理。可以看出run_graph在这里是递归调用,所以graph上的所有filter,当然前提是graph创建的按照规则创建的,那么所有的filter的desc的process接口都将被调用执行。等推出run_graph,在run_graphs中会检查unschedulable变量,我们之前将所有没有处理的filter都放到该变量的链表上了,对于unschedulable继续调用run_graphs,可以看出这里run_graphs也是递归调用。等退出run_graphs,那么所有第一次可以处理的和第一次没有处理后续可以处理的filter都被处理过了,至此沿着queue建立的链接graph对应的filter通路就被走了一遍。

回到ms_ticker_run,ticker的time被增加ticker的interval变量指示的值。之后判断time和ticker运行的实际时间的差值,如果大于零,说明ticker在本次interval时间值到达之前就已经处理完所有filter了,那么就调用sleepMs打发掉这个差值。如果差值不大于零,说明休息时间到了,需要注意的是这里是在while(1)中处理的,肯定会出现小于等于零的情况,如果差值的绝对值过大,则说明休息过头了,会打印警告信息,跳出while(1)的处理接着处理下次的run_graphs。

对于RTP部分的传送,就需要看看filter的process怎么处理,对其他filter也一样。

数据是在filter的process中传递的,因为filter基本上都是靠msqueue连接在一起的,所以数据的传递也基本是通过queue的m_blk来完成的。关于m_blk的构造,参考ortp分析。

之前是在linphonecore没有配置use_files的情况下直接使用声卡设备来获取和播放数据的,如果配置了上述变量,linphonecore将用文件来代替声卡设备,此时调用audio_stream_start_with_files接口,将文件指针传递进去以取代声卡设备。内部处理逻辑基本上同使用声卡设备的处理,这里就不再进行说明了。

调用接口post_configure_audio_streams对audio stream进行后期的一些处理。在该接口中主要读取配置文件或者sound.conf结构体上的许多配置信息,调用filter的call methods接口,通过id选择相应的接口对filter进行配置。这里面配置项比较多繁杂,就不在详细说明。但是这里的配置可能比较影响最终的音频传输效果。

调用接口audio_stream_set_rtcp_information配置rtcp的版本信息。

至此音频处理完了,接着处理视频部分。视频部分流程基本同音频部分。

最后将call的state设置为LCStateAVRunning。返回。

所有上述这些处理都是在sal_iterate的process_event中完成的。是当从底层取上来一个CALL_ANSWER事件触发上层任务来处理。接着调用authentication_ok后就跳出了process_envent。也就跳出了sal_iterate,到linphone_core_iterate中继续执行。

此时linphonecore上的call不再为空,而且状态也为LCStateAVRunning了,在该状态下,linphonecore会在调试状态下显示当前的带宽使用情况,对于视频调用video_stream_iterate进行流量控制,对于音频调用audio_stream_alive保持连接畅通。

至此,协议的处理和音视频的处理以及二者的关联也就都完成了。

3 Answer过程分析

事件的触发:对端发送invite请求给客户端,请求建立sip协商与连接,呼叫客户端,建立视频通话。

数据到达底层硬件,接收到后缓存给上层应用。

系统此时已经启动了exosip_thread线程,该线程循环运行,每次都会调用接收接口检查底层是否有数据到达。因此,当invite请求数据到来时,exosip会调用eXosip_read_message接口通过底层socket接口接收数据,并解析数据,将解析的结果根据软件架构和sip状态机及事务处理机制存放到各个osip_transaction上。这块基本处理逻辑同上面的响应处理部分。

底层的sip处理模块,包括osip和exosip,处理invite请求,通过状态机处理各种需要处理的状态,并发送响应给请求端。当底层SIP模块对sip消息处理差不多时,会将进一步的处理请求交给上层linphone core任务,因为它还要急着处理接收和发送sip消息的任务,至于上层的一些处理它就不管了。这种角色转换是通过发送一个事件到事务链表上完成的。

上层linphone core任务每隔一秒就会迭代处理sal模块和音视频模块。在sal模块,此时会接收到一个事务上的事件,并进入事件处理中。

从代码中的关联来看,exosip任务处理osip事务状态机上的事件,完成后调用回调函数,回调函数将剩余的处理交给exosip的事件队列等待main任务主线程去处理。回调函数在文件jcallback.c中,udp.c也有一些。上层事件处理在sal_exosip2.c中,事件定义在exosip.h中,因此可以通过关联这三个文件和打印来跟踪底层消息和上层消息之间的流动和传递。

关于上传给上层的事件:

注册成功,上传1

Keepalive过程,上传27

底层协商invite通路打通,进行媒体处理,上传5,建立一个新的call.

对于invite,关键的一个事件处理是EXOSIP_CALL_INVITE类型的事件。也正如上面的介绍,当会话请求是由对端发起时,底层exosip任务接收到数据包后查找事件队列此时并不能找到针对该响应的事件队列,此时会调用exosip_process_newrequest来处理,在该接口中如果判断到是invite消息时会调用exosip_process_new_invite接口处理。在new_invite中处理完必要的初始化工作后,会调用report_call_event将exosip_call_invite消息上报。对于该事件,linphone core调用inc_new_call来处理。

offer_answer_initiate_outgoing接口创建一个salmediea媒体描述符。在这里我们会获取我们本地支持的音视频编解码类型,来和对端sdp中给出的编解码类型进行比较,如果有匹配的,说明相互音视频编解码的类型协商成功,后续进行音视频处理时就用协商好的类型。如果音视频类型没有在本地找到,也就是说对端发起invite请求时给定的类型本地都不支持,对于linphone来讲,不会对这种错误进行其他措施来努力使协商成功,而是认为当前sip协商不成功,无法建立sal media媒体描述符。此时一个空的媒体描述符会被返回,后续处理中除了给对端发送415错误状态外,linphone core还会将call,就是刚才建立的call销毁,取消本次会话。(这在调试客户端时遇到了,当添加上对之前不支持媒体类型的支持后,即使支持是错误的,也能够使sip会话成功,只是后续媒体流就会有问题。另外,sip客户端的等待answer的时间有点短,这个需要调整一下。)

4 关于RTP及音视频流的网络传输

在linphone中,RTP传输是通过ortp库完成的。Ortp库的初始化在linphone初始化时就调用了,为ortp_init。要基于RTP传输音视频,初始化完成后最主要的一步就是创建RTP会话结构体,也就是rtpsession,后续所有的有关RTP传输方面的配置等都是基于该session来完成的。

这部分先从RTPfilter看起,进一步的从rtpsender的filter来看。

在sender_process接口中,从参数filter上可以得到私有数据SenderData,从SenderData上可以得到rtpsession,这个session就是之前初始化时创建的。

如果session为空,就是没有session,则直接调用ms_queue_flush扔掉到该filter的队列的数据直接返回。否则继续。

在while循环中处理filter上的所有数据。这里是调用ms_queue_get从filter的inputs队列上取mblk类型的数据块。如果不为空就一直取,作为while循环的条件,所以在该循环中会取光队列上的所有数据块并对其进行处理。

根据filter和数据块调用get_cur_timestamp得到当前的时间戳。如果私有数据部分定义了skip,调用send_dtmf发送dtmf信息。

如果私有数据的skip和mute_mic有一个不为FALSE就简单的调用freemsg释放掉该数据块,否则取得该数据块中承载的payloadtype的number号,调用rtp_session_create_packet创建一个RTP数据包。如果payload type的number号大于0,那么调用rtp_set_payload_type将RTP header的payloadtype设置为该值。这里实际上是将RTP的header封装到mblk中的,该结构体的b_cont指针指向当前从队列中取得的mblk上。接着调用rtp_session_sendm_with_ts发送数据。

有关rtp_session_sendm_with_ts发送的处理,参见ortp分析文档。

数据发送完成后,对RTP的senderdata私有数据进行一些配置,接着继续下次的while循环处理。

其实基本的流程还是挺简单的,就是从filter上不断的取mblk,然后基于当前会话构造RTP数据包,并将其发送出去。外部的其他数据就包括filter上的私有数据senderdata。而session早在sip协商过程中就在媒体初始化中完成了。

看完了发送部分,接着再看接收部分。RTP数据流的接收在filter的receiver_process中处理。首先计算一个时间戳值,接着在while中掉用rtp_session_recvm_with_ts接收数据包,这作为一个循环条件,只要session上的数据没有接收完就一直接收。数据接收上来后,对于一个mblk,从RTP头中取得时间戳保存到mblk的reserved1中,取得markbit和payload type放到reserved2中。接着调用rtp_get_payload得到真正的payload数据,实际上就是让数据开始指针跳过RTP头,指向真正的数据区开始部分,并重新设置数据的长度。这些都是在mblk自身上进行的,没有创建新的mblk。最后将调整后的mblk放到filter的output0中,交给下一个filter去处理。这是通过调用ms_queue_put完成的,filter的output是个queue。

同样,对于取数据的接口rtp_session_recvm_with_ts,处理步骤参考ortp分析文档。

至此,RTP的接收和发送就基本上理通了。

5 总结

创建或者初始化一个新的会话主要并且必须进行的操作步骤包括:

linphone_call_new_incoming:创建新的call结构体

创建新的sal结构体

sal_call_set_local_media_description:添加描述信息

linphone_core_init_media_streams:创建audio 和video stream结构体,并设置参数

Sal开头的后续操作解决协议协商问题

Videostreamstart等相关接口处理流的filter创建和link以及atacher到ticker上,启动RTP数据传输。

初始化基本完成后,会有响铃提示音,不管是拨出着还是拨入,都会响铃。对于拨入,响铃提示音提示输入answer,linphone据此会进行下一步的处理,如果超时时间达到,还没有输入answer,就会自动terminate当前的call。

用户输入answer后,调用lpc_cmd_answer进行该命令的处理,内部调用linphone_core_accept_call处理会话相关逻辑。此时首先停止ring stream,也就是响铃流。其次调用sal_call_accept发送acknowledge给对端,最后调用linphone_core_start_media_streams启动流的传输。

对于主动拨出的会话,sal在处理events的时候会处理到ring事件,然后调用ring_start启动ring stream,之后客户端会提示响铃。当对端输入answer后,本地在sal迭代处理时处理EXOSIP_CALL_ANSWERED消息,该消息表明对端输入了answer接受了invite。在该消息的处理中,调用call_accepted,在call_accepted中检查时间链上的response,处理流,构建并发送acknowledge响应,之后调用call_accepted回调函数。在回调函数中调用linphone_connect_incoming处理输入流请求。在该接口处理中,显示connected状态,设置call的状态为LCStateAVRunning,如果ringstreamer还存在,则调用ring_stop停止响铃。之后调用linphone_core_start_media_streams启动流媒体传输处理。

对于linphone,当call创建后,只有三个状态来处理,一个是超时的invite重发,一个是响铃处理,一个是视频流的监控。Call的state主要在每次的迭代处理中进行检查。主要有init状态,preestablishing状态,ringing状态和avrunning状态。第一个为初始化,在init一个call时,或者发起invite时用,第二个为sip会话协商,在options ping中用,第三个为响铃状态,第四个为音视频流处理状态。

Linphone core 全局状态中有关call的状态主要指明当前的linphone core上的call处于idle、输出invite,输出connected、输入invite、输入connected、end、error、invalid、输出ringing等状态。其实表明了当前会话协商的状态如何。这些状态,严格来说,针对每个call都会有一套类似的状态处理,只是当前版本还不支持多call会话而已。

对于当前这种只支持一个call实例的情况,idle,invite状态会与linphone的init prestablishing状态重叠,而输出ringing状态和linphoen的ringing状态重叠,connected状态后linphone也就到了avrunning状态了。

最后,在看代码过程中,需要区分linphone core、call以及sal三个实例。

Linphone分析相关推荐

  1. Linphone分析 1_初始化

    文章目录 1. linphone的初始化流程 1. 初始化流程图: 2. 上下文环境: Sal 1. Sal的初始化流程 1.Sal构造所初始化全局成员的流程 2. 代码流程分析 3 linphone ...

  2. 智能会议系统(24)---linphone的架构和初始化

    linphone 分析1 linphone的架构和初始化 1.linphone 包含的库 1 ReadLine 一个终端显示库, Linphone 会用到它时里面的事件循环机制来读取会话事件. 2 f ...

  3. 智能会议系统(25)---linphone代码分析

    linphone代码分析 最近在做linphone移植到hi3516d的工作,花了些时间弄懂了linphone和media2stream的运行过程,在这里分享出来,希望可以和大家一起探讨. 1.代码架 ...

  4. Linphone android去电增加自定义SIP消息头的流程分析

    一.首先看一下如何在发起去电的sip请求中添加自定义的消息头 增加自定义头消息发方法,so已经提供了native方法, 发起呼叫的示例如下: LinphoneCallParams params = l ...

  5. linphone录音分析

    linphone录音分析1 旧版linphone被叫通话过程 新版linphone被叫通话过程 在被叫通话时实现录音 开始录音 answerRecord() acceptCall(Call call) ...

  6. Linphone录音器的初始化流程分析

    初始化入口: linphone_core_init() --linphonecore.c 1793 static void linphone_core_init(LinphoneCore * lc, ...

  7. linphone代码分析

    Linphone代码分析 一,Linphone4.0编译android版本 (环境ubuntu1864bits) 安装下列包: 1     sudo apt-get install yasm 2    ...

  8. linPhone 源码分析

    http://www.360doc.com/content/13/1125/17/2306903_332089963.shtml 这几天比较轻松,所以打算好好来看看linphone的代码,源码版本为3 ...

  9. linphone android 分析,Android平台上的Linphone学习(一)

    Linphone: 适用于很多平台(Windows, Mac OS, Android)的VOIP电话工具, 基于标准SIP协议. Linphone-android: Android平台上的Linpho ...

最新文章

  1. OpenCV 多层感知器训练代码示例
  2. 现实交互动作和现实环境交互的魅力
  3. xshell使用xftp传输文件、使用pure-ftpd搭建ftp服务
  4. TCP/IP总结(4)TCP 之数据包格式
  5. Google、Azure、阿里云、RedHat…全球的 K8s 圈大佬聚在一起要聊啥?
  6. maven配置环境变量失败解决办法
  7. 在.NET Core 上运行的 WordPress
  8. eclipse安装java web插件
  9. scratch跳一跳游戏脚本_cocos creator制作微信小游戏「跳一跳」
  10. java 缓冲区中的数据存入缓冲区中_java8中NIO缓冲区(Buffer)的数据存储详解|chu...
  11. 利用动态规划(DP)解决 Coin Change 问题
  12. java 过滤js事件_java中的过滤器与监听器
  13. 5.1傅里叶展开,傅里叶级数推导--非常棒
  14. wps ppt, 版式与母版
  15. Vmware打开服务器的时候提示“该虚拟机似乎正在使用中。”
  16. 华为手机怎么强制关机_华为手机怎么强制关机
  17. ngrok穿透服务器搭建
  18. 《实用VC编程之玩转控件》第15课:Tree树形控件
  19. 练习-Java字符串之String类常用方法之文件名与邮箱验证
  20. [Android]安卓上传下载文件

热门文章

  1. Docker学习笔记20:docker使用之资源汇总
  2. Customizable constraint systems for succinct arguments学习笔记(2)
  3. 力扣算法刷题Day30|回溯:重新安排行程 N皇后 解数独
  4. 星级豪华酒店如何提高利润回报率
  5. Vue相关组件的安装
  6. 算法学习--布隆过滤器
  7. Archlinux + Gnome 安装教程
  8. Windows系统如何配置pycharm的anaconda环境
  9. 力控——从运动控制到交互控制
  10. 【LWJGL官方教程】Game loops 游戏循环