在学习笔记4中,我们研究了pjmedia_stream对象
struct pjmedia_transport
{
/** Transport name (for logging purpose). */
char name[PJ_MAX_OBJ_NAME];
/** Transport type. */
pjmedia_transport_type type;
/** Transport's "virtual" function table. */
pjmedia_transport_op *op;
/** Application/user data */
void *user_data;
};
注意到: "virtual" function, 很明显, pjmedia_transport必须实现标准接口函数族
pjmedia_transport 就像声音设备抽象层 pjmedia_aud_stream或者pjmedia_port一样, 是一个抽象层
在pjsip源代码的pjmedia核心代码, 我们找到了以下pjmedia_transport的实现:
transport_adapter_sample.c
transport_ice.c
transport_loop.c
transport_srtp_dtls.c 注意这个派生“类”均把pjmedia_transport base成员作为第一个成员
transport_srtp_sdes.c 目的是通过对pjmedia_transport*指针强制转换,可以获得“派生”对象的指针
transport_srtp.c 例如: transport_udp* tp_udp = (transport_udp*)pbase;
transport_udp.c 这个转换通常是用在pjmedia_transport_op函数族的实现代码中
以transport_udp为例, 派生对象的定义如下, 机构体内不出所料,包含了网络传输所需要的网络地址以及套接字相关的定义:
struct transport_udp
{
pjmedia_transport base; /**< Base transport. */
pj_pool_t *pool; /**< Memory pool */
unsigned options; /**< Transport options. */
unsigned media_options; /**< Transport media options. */
void *user_data; /**< Only valid when attached */
//pj_bool_t attached; /**< Has attachment? */
pj_sockaddr rem_rtp_addr; /**< Remote RTP address */
unsigned rem_rtp_cnt; /**< How many pkt from this addr. */
pj_sockaddr rem_rtcp_addr; /**< Remote RTCP address */
int addr_len; /**< Length of addresses. */
void (*rtp_cb)( void*, /**< To report incoming RTP. */
void*,
pj_ssize_t);
void (*rtcp_cb)( void*, /**< To report incoming RTCP. */
void*,
pj_ssize_t);
unsigned tx_drop_pct; /**< Percent of tx pkts to drop. */
unsigned rx_drop_pct; /**< Percent of rx pkts to drop. */
pj_sock_t rtp_sock; /**< RTP socket */
pj_sockaddr rtp_addr_name; /**< Published RTP address. */
pj_ioqueue_key_t *rtp_key; /**< RTP socket key in ioqueue */
pj_ioqueue_op_key_t rtp_read_op; /**< Pending read operation */
unsigned rtp_write_op_id;/**< Next write_op to use */
pending_write rtp_pending_write[MAX_PENDING]; /**< Pending write */
pj_sockaddr rtp_src_addr; /**< Actual packet src addr. */
unsigned rtp_src_cnt; /**< How many pkt from this addr. */
int rtp_addrlen; /**< Address length. */
char rtp_pkt[RTP_LEN];/**< Incoming RTP packet buffer */
pj_sock_t rtcp_sock; /**< RTCP socket */
pj_sockaddr rtcp_addr_name; /**< Published RTCP address. */
pj_sockaddr rtcp_src_addr; /**< Actual source RTCP address. */
unsigned rtcp_src_cnt; /**< How many pkt from this addr. */
int rtcp_addr_len; /**< Length of RTCP src address. */
pj_ioqueue_key_t *rtcp_key; /**< RTCP socket key in ioqueue */
pj_ioqueue_op_key_t rtcp_read_op; /**< Pending read operation */
pj_ioqueue_op_key_t rtcp_write_op; /**< Pending write operation */
char rtcp_pkt[RTCP_LEN];/**< Incoming RTCP packet buffer */
};
二) transport_udp 传输层代码分析:
类比声卡抽象层, transport_udp的pjmedia_transport抽象接口实现一定包含了网络套接子的启动和释放代码, 网络数据收发代码
我们来看看 transport_udp中pjmedia_transport_udp_create3()函数中有关对pjmedia_transport抽象接口函数的实现代码
1) 建立RTCP和RTP数据收发套接子, 然后调用pjmedia_transport_udp_attach进一步初始化(以下步骤发生在pjmedia_transport_udp_attach中)
2) 使用pjmedia_endpt_get_ioqueue(endpt)获得一个ioqueue , 供pj_ioqueue_poll()进行套接子列表轮询用
3) jmedia_transport抽象接口函数族实现如下:
static pjmedia_transport_op transport_udp_op = {
&transport_get_info,
&transport_attach,
&transport_detach,
&transport_send_rtp,
&transport_send_rtcp,
&transport_send_rtcp2,
&transport_media_create,
&transport_encode_sdp,
&transport_media_start,
&transport_media_stop,
&transport_simulate_lost,
&transport_destroy
};
4)修正下RTP/RTCP套接子所使用的地址(对方需要一个确定的地址, 而不像本地端口绑定可以使用0.0.0.0这个地址)
5)使用pj_ioqueue_register_sock函数把RTP/RTCP套接子注册到到轮询对列ioqueue中, 同时注册了相应的回调接口,并返回一个唯一的pj_ioqueue_key_t作为回调函数入口参数识别socket数据来源
6)在pj_ioqueue中,套接子回调接口实际上是一个函数族, 定义如下:
typedef struct pj_ioqueue_callback
{
void (*on_read_complete)(pj_ioqueue_key_t *key, pj_ioqueue_op_key_t *op_key, pj_ssize_t bytes_read);
void (*on_write_complete)(pj_ioqueue_key_t *key, pj_ioqueue_op_key_t *op_key, pj_ssize_t bytes_sent);
void (*on_accept_complete)(pj_ioqueue_key_t *key, pj_ioqueue_op_key_t *op_key, pj_sock_t sock, pj_status_t status);
void (*on_connect_complete)(pj_ioqueue_key_t *key, pj_status_t status);
} pj_ioqueue_callback;
我们果然在函数入口参数中其中找到了2)中提到的pj_ioqueue_key_t
在transport_udp, 函数族中,只实现了on_read_complete, 也就是针对RTP的on_rx_rtp() 和 针对RTCP的on_rx_rtcp()
*** 至于套接子数据的发送, 则由jmedia_transport抽象接口函数的以下三个函数实现
send_rtp,
send_rtcp,
send_rtcp2,
为了不破坏封装,pjmedia建议使用pjmedia_transport_send_xxx()系列函数,实现对各种接口在发送数据的形式上实现统一
在笔记4中,我们曾经提到过:
pjmedia_transport层发送的数据, 则是由pjmedia_stream主动调用以下两个函数实现的
pjmedia_transport_send_rtp 调用是这样滴: put_frame->put_frame_imp->pjmedia_transport_send_rtp
pjmedia_stream对象的功能和接口在这张图上很清楚了
https://trac.pjsip.org/repos/wiki/media-flow#IncomingRTPRTCPPackets
下面学习下传输层对象
struct pjmedia_transport
{
/** Transport name (for logging purpose). */
char name[PJ_MAX_OBJ_NAME];
/** Transport type. */
pjmedia_transport_type type;
/** Transport's "virtual" function table. */
pjmedia_transport_op *op;
/** Application/user data */
void *user_data;
};
注意到: "virtual" function, 很明显, pjmedia_transport必须实现标准接口函数族
pjmedia_transport 就像声音设备抽象层 pjmedia_aud_stream或者pjmedia_port一样, 是一个抽象层
在pjsip源代码的pjmedia核心代码, 我们找到了以下pjmedia_transport的实现:
transport_adapter_sample.c
transport_ice.c
transport_loop.c
transport_srtp_dtls.c 注意这个派生“类”均把pjmedia_transport base成员作为第一个成员
transport_srtp_sdes.c 目的是通过对pjmedia_transport*指针强制转换,可以获得“派生”对象的指针
transport_srtp.c 例如: transport_udp* tp_udp = (transport_udp*)pbase;
transport_udp.c 这个转换通常是用在pjmedia_transport_op函数族的实现代码中
以transport_udp为例, 派生对象的定义如下, 机构体内不出所料,包含了网络传输所需要的网络地址以及套接字相关的定义:
struct transport_udp
{
pjmedia_transport base; /**< Base transport. */
pj_pool_t *pool; /**< Memory pool */
unsigned options; /**< Transport options. */
unsigned media_options; /**< Transport media options. */
void *user_data; /**< Only valid when attached */
//pj_bool_t attached; /**< Has attachment? */
pj_sockaddr rem_rtp_addr; /**< Remote RTP address */
unsigned rem_rtp_cnt; /**< How many pkt from this addr. */
pj_sockaddr rem_rtcp_addr; /**< Remote RTCP address */
int addr_len; /**< Length of addresses. */
void (*rtp_cb)( void*, /**< To report incoming RTP. */
void*,
pj_ssize_t);
void (*rtcp_cb)( void*, /**< To report incoming RTCP. */
void*,
pj_ssize_t);
unsigned tx_drop_pct; /**< Percent of tx pkts to drop. */
unsigned rx_drop_pct; /**< Percent of rx pkts to drop. */
pj_sock_t rtp_sock; /**< RTP socket */
pj_sockaddr rtp_addr_name; /**< Published RTP address. */
pj_ioqueue_key_t *rtp_key; /**< RTP socket key in ioqueue */
pj_ioqueue_op_key_t rtp_read_op; /**< Pending read operation */
unsigned rtp_write_op_id;/**< Next write_op to use */
pending_write rtp_pending_write[MAX_PENDING]; /**< Pending write */
pj_sockaddr rtp_src_addr; /**< Actual packet src addr. */
unsigned rtp_src_cnt; /**< How many pkt from this addr. */
int rtp_addrlen; /**< Address length. */
char rtp_pkt[RTP_LEN];/**< Incoming RTP packet buffer */
pj_sock_t rtcp_sock; /**< RTCP socket */
pj_sockaddr rtcp_addr_name; /**< Published RTCP address. */
pj_sockaddr rtcp_src_addr; /**< Actual source RTCP address. */
unsigned rtcp_src_cnt; /**< How many pkt from this addr. */
int rtcp_addr_len; /**< Length of RTCP src address. */
pj_ioqueue_key_t *rtcp_key; /**< RTCP socket key in ioqueue */
pj_ioqueue_op_key_t rtcp_read_op; /**< Pending read operation */
pj_ioqueue_op_key_t rtcp_write_op; /**< Pending write operation */
char rtcp_pkt[RTCP_LEN];/**< Incoming RTCP packet buffer */
};
二) transport_udp 传输层代码分析:
类比声卡抽象层, transport_udp的pjmedia_transport抽象接口实现一定包含了网络套接子的启动和释放代码, 网络数据收发代码
我们来看看 transport_udp中pjmedia_transport_udp_create3()函数中有关对pjmedia_transport抽象接口函数的实现代码
1) 建立RTCP和RTP数据收发套接子, 然后调用pjmedia_transport_udp_attach进一步初始化(以下步骤发生在pjmedia_transport_udp_attach中)
2) 使用pjmedia_endpt_get_ioqueue(endpt)获得一个ioqueue , 供pj_ioqueue_poll()进行套接子列表轮询用
3) jmedia_transport抽象接口函数族实现如下:
static pjmedia_transport_op transport_udp_op = {
&transport_get_info,
&transport_attach,
&transport_detach,
&transport_send_rtp,
&transport_send_rtcp,
&transport_send_rtcp2,
&transport_media_create,
&transport_encode_sdp,
&transport_media_start,
&transport_media_stop,
&transport_simulate_lost,
&transport_destroy
};
4)修正下RTP/RTCP套接子所使用的地址(对方需要一个确定的地址, 而不像本地端口绑定可以使用0.0.0.0这个地址)
5)使用pj_ioqueue_register_sock函数把RTP/RTCP套接子注册到到轮询对列ioqueue中, 同时注册了相应的回调接口,并返回一个唯一的pj_ioqueue_key_t作为回调函数入口参数识别socket数据来源
6)在pj_ioqueue中,套接子回调接口实际上是一个函数族, 定义如下:
typedef struct pj_ioqueue_callback
{
void (*on_read_complete)(pj_ioqueue_key_t *key, pj_ioqueue_op_key_t *op_key, pj_ssize_t bytes_read);
void (*on_write_complete)(pj_ioqueue_key_t *key, pj_ioqueue_op_key_t *op_key, pj_ssize_t bytes_sent);
void (*on_accept_complete)(pj_ioqueue_key_t *key, pj_ioqueue_op_key_t *op_key, pj_sock_t sock, pj_status_t status);
void (*on_connect_complete)(pj_ioqueue_key_t *key, pj_status_t status);
} pj_ioqueue_callback;
我们果然在函数入口参数中其中找到了2)中提到的pj_ioqueue_key_t
在transport_udp, 函数族中,只实现了on_read_complete, 也就是针对RTP的on_rx_rtp() 和 针对RTCP的on_rx_rtcp()
*** 至于套接子数据的发送, 则由jmedia_transport抽象接口函数的以下三个函数实现
send_rtp,
send_rtcp,
send_rtcp2,
为了不破坏封装,pjmedia建议使用pjmedia_transport_send_xxx()系列函数,实现对各种接口在发送数据的形式上实现统一
在笔记4中,我们曾经提到过:
pjmedia_transport层发送的数据, 则是由pjmedia_stream主动调用以下两个函数实现的
pjmedia_transport_send_rtp 调用是这样滴: put_frame->put_frame_imp->pjmedia_transport_send_rtp
至此,我们基本上算搞清楚了声音是如何从alsa声卡到会议桥再到传输层的整个过程。
遗留问题:pj_ioqueue_poll()进行套接子列表轮询是在哪里调用的呢?在编码器框架中会讲到该问题
https://trac.pjsip.org/repos/wiki/media-flow#IncomingRTPRTCPPackets中由提到:
The flow of incoming RTP packets are like this:
- an internal worker thread in the Media Endpoint polls the IOQueue where all media transports are registered to.
- when an incoming packet arrives, the poll function will trigger on_rx_rtp() callback of the UDP media transport to be called. This callback was previously registered by the UDP media transport to the ioqueue.
- the on_rx_rtp() callback reports the incoming RTP packet to the media stream port. The media stream was attached to the UDP media transport during session initialization by application.
- the media stream unpacks the RTP packet using its internal RTP session, update RX statistics, de-frame the payload according to the codec being used (there can be multiple audio frames in a single RTP packet), and put the frames into the jitter buffer.
- the processing of incoming packets stops here, as the frames in the jitter buffer will be picked up by the main flow (a call to pjmedia_port_get_frame() to the media stream.