在学习笔记3中,我们学习了会议桥,了解了会议桥的相关机制
1)会议桥具有多个conf_port, 使用ports[]数组保存
2)会议桥实现了一个叫做master_port的pjmedia_port, 占用ports[0]的conf_port资源
3)会议桥可能会创建一个pmedia_snd_port对象, 通过pjmedia_snd_port_connect,连接到master_port,打通了声卡设备到会议桥的数据通道
4) 会议桥除ports[0]的conf_port端口,都可以连接到一个pmedia_port对象,实现与外部的音频数据的交换
5)会议桥所有的conf_port端口, 可以通过pjmedia_conf_connect_port调用建立连接关系, 连接是多对多的关系
当conf_port[0]作为监听端口 具有多个源conf_port端口时, 监听master_port上就会进行混音操作
因为会议桥代码提供的get_frame()只供master_port接口使用, 所以会议桥内部混音操作只会发生在master_port接口的get_frame函数中
get_frame函数实际上成了混音操作的节拍器, 它会遍历每一个监听端口, 对每个监听端口进行混音, 混音结果使用write_port()->pjmedia_port_put_frame()输出。
混音其里面有个宏SIMPLE_AGC(cport->last_mix_adj, cport->mix_adj);用来实现进行混音AGC
接下来研究下连接在会议桥端口上的stream对象:
仍然参考这张图https://trac.pjsip.org/repos/wiki/media-flow#IncomingRTPRTCPPackets
本次重点放在图中位于会议桥和传输层之间的stream.c上面
一) 先研究下libpjsib提供的两个例子
在conference.c例子中, 为我们演示了一个具有file_count个play文件流,RECORDER个录制文件流的混音器
混音器总端口数=file_count + 1 + RECORDER, 对外可用的端口数为file_count + RECORDER, 内部master_port占用端口0
1) pjmedia_wav_writer_port_create + pjmedia_conf_add_port 添加了一个录制文件流
2) pjmedia_wav_player_port_create + pjmedia_conf_add_port 添加了file_count个录制文件流
3) 端口之间的连接关系通过终端控制台进行配置(pjmedia_conf_connect_port/pjmedia_conf_disconnect_port)
4) 通过pjmedia_conf_adjust_tx_level和pjmedia_conf_adjust_rx_level调整输出输入通道的音量调整范围(-128 to 127) --- 这个由猜测的成分,大概是吧, 原理还未搞明白
例子程序confbench.c则为我们演示另一个混音器, 这个混音器内部不创建pjmedia_snd_port实例(PJMEDIA_CONF_NO_DEVICE):
status = pjmedia_conf_create( pool, PORT_COUNT, CLOCK_RATE, 1, SAMPLES_PER_FRAME, 16, PJMEDIA_CONF_NO_DEVICE, &conf);
1)会议桥端口总数为PORT_COUNT=254个
2)会议桥端口添加了NULL_COUNT=100个0数据端口: pjmedia_null_port_create + pjmedia_conf_add_port
3)会议桥端口添加了SINE_COUNT=100个正玄波端口: pjmedia_conf_add_port + pjmedia_conf_add_port
4)会议桥端口添加了IDLE_COUNT=32个0数据端口作为空闲端口: pjmedia_null_port_create + pjmedia_conf_add_port
5)获得会议端口0: pjmedia_conf_get_master_port
6)因为未启动声卡,会议桥创建一个pjmedia_master_port作为混音节拍器:pjmedia_master_port_create, 使用端口0作为下行stream端口,第一个null_port做上行stream源端口
7)启动混音节拍器master_port: pjmedia_master_port_start----将启动一个精确时钟线程作为混音节拍器
8)时钟线程每间隔指定时间, 进行一次回调(clock_callback在pjmedia_master_port定义)
在clock_callback中, 实现功能如下:
i> Get frame from upstream port and pass it to downstream port 其中upstream就是null_port[0] ,还会给从master_port端口获得的帧打上时间标签
ii> Get frame from downstream port and pass it to upstream port 而downstream就是master_port,也就是conf->master_port
通过着两个例子, 可以看到, 会议桥的各个“端口”所连接的对象, 都必须实现一个pjmedia_port接口, 端口之间的音频数据流动需要节拍器进行驱动
如果会议桥启动了pjmedia_snd_port实例,则pjmedia_snd_port实例就是这个驱动的动力源, 否则应当创建一个时钟线程连接到master_port作为驱动源
二) pjmedia_stream (stream.c)
根据https://trac.pjsip.org/repos/wiki/media-flow#IncomingRTPRTCPPackets提供的图片
stream.c在传输层和会议桥之间起到承上启下的作用,并实现了一个jitter_buffer
前面说过: 会议桥的各个“端口”所连接的对象, 都必须实现一个pjmedia_port接口
所以 stream.c也应该实现了这个pjmedia_port接口, 查阅代码, 果然后get_frame/putt_frame的函数定义
在struct pjmedia_stream结构体中, 也找到了接口的定义:
pjmedia_port port;
pjmedia_transport *transport;//本文是想研究音频数据如何从会议桥到达网络的, 所以这里还关注了与传输层有关成员
看下pjmedia_stream_create()函数原型 PJ_DEF(pj_status_t) pjmedia_stream_create( pjmedia_endpt *endpt,
pj_pool_t *pool,
const pjmedia_stream_info *info,
pjmedia_transport *tp,
void *user_data,
pjmedia_stream **p_stream)
可见 pjmedia_transport *tp, 是在pjmedia_stream外部创建的对象,粗略地阅读下pjmedia_stream_create代码, 了解到以下信息:
1) 如果外部未提供内存池 ,则pjmedia_stream内部自己建立一个,并保存在own_pool成员中
2) 入口参数pjmedia_stream_info *info提供了编解码器参数, 保存在si成员中
3) 入口参数pjmedia_stream_info *info还提供了接口pjmedia_port的初始化参数
4) 入口参数pjmedia_endpt *endpt提供编解码管理所需的media_endpt,保存在成员endpt中
5) 编解码管理器保存在成员codec_mgr中: stream->codec_mgr = pjmedia_endpt_get_codec_mgr(endpt)
6) 创建并初始化了pjmedia_stream的编解码其实例: pjmedia_codec_mgr_alloc_codec+pjmedia_codec_init+pjmedia_codec_open
7) 对pmedia_port接口操作函数赋值, 注意到,当RTP数据为非压缩的PJMEDIA_FORMAT_L16格式时的get_frame接口与压缩的格式时
与编码格式时使用的get_frame接口函数是不同的: get_frame() / get_frame_ext()
8) 建立了jitter缓冲区(pjmedia_jbuf_create) 还有相应的操作mutext(pj_mutex_create_simple)
9) 建立数据编码通道: create_channel( pool, stream, PJMEDIA_DIR_ENCODING, info->tx_pt, info, &stream->enc);//tx_pt应该与RTP负载类型有关
10) 建立数据解码通道: create_channel( pool, stream, PJMEDIA_DIR_DECODING, info->rx_pt, info, &stream->dec); //tx_pt应该与RTP负载类型有关
11) 最后,关联一个 pjmedia_transport,用于数据发送: pjmedia_transport_attach2(tp, &att_param);
att_param.rtp_cb = &on_rx_rtp; //数据RTP接收操作在这里
att_param.rtcp_cb = &on_rx_rtcp; //数据RTCP接收操作在这里
总结一下, pjmedia_stream实现了pjmedia_port接口, attach了一个pjmedia_transport, 建立了编解码通道
留下的疑问:
1) 有on_rx_rtp回调, 没有on_tx_rtp回调
2) 有on_rx_rtcp回调, 没有on_tx_rtcp回调
3) 编解码通道是个什么东西?
==看看pjmedia_transport_attach2的代码我们就明白了
PJ_INLINE(pj_status_t) pjmedia_transport_attach2(pjmedia_transport *tp,
pjmedia_transport_attach_param *att_param)
{
if (tp->op->attach2) {
return tp->op->attach2(tp, att_param);
} else {
return tp->op->attach(tp, att_param->user_data,
(pj_sockaddr_t*)&att_param->rem_addr,
(pj_sockaddr_t*)&att_param->rem_rtcp,
att_param->addr_len, att_param->rtp_cb,
att_param->rtcp_cb);
}
}
原来pjmedia_stream提供的两个on_rx_rtp回调和on_rx_rtcp回调,通过attach函数交给pjmedia_transport层使用去了
pjmedia_transport层是网络层, 每当接收到RTP/RTCP数据包后, 就用过着两个回调函数,使数据下行到pjmedia_stream
至于向pjmedia_transport层发送的数据, 则是由pjmedia_stream主动调用以下两个函数实现的
pjmedia_transport_send_rtp 调用是这样滴: put_frame->put_frame_imp->pjmedia_transport_send_rtp
pjmedia_transport_send_rtcp
==再看看编解码通道创建函数pjmedia_stream_create, 核心代码在这里:
channel = PJ_POOL_ZALLOC_T(pool, pjmedia_channel);
。。。
pjmedia_rtp_session_setting settings;
settings.flags = (pj_uint8_t)((param->rtp_seq_ts_set << 2) | 3);
settings.default_pt = pt;
settings.sender_ssrc = param->ssrc;
settings.seq = param->rtp_seq;
settings.ts = param->rtp_ts;
status = pjmedia_rtp_session_init2(&channel->rtp, settings);
。。。
channel->rtp指向一个RTP包头的封装和解封装对象
再回头看看上面提过的函数put_frame_imp,发送到传输层的数据前进行RTP头部封装, 然后使用负载数据再用编码器进行编码(例如g711编码):
先调用:pjmedia_codec_encode 编码
在调用:pjmedia_rtp_encode_rtp封装
而在on_rx_rtp回调函数的实现中, 我们看到了RTP解封装: pjmedia_rtp_decode_rtp , 解封的数据被到jitter_buffer中
在get_frame()/get_frame_ext()函数中, 我们看到了音频数据解码: pjmedia_codec_decode, 待解码数据来自jitter_buffer