pjsip学习笔记4

在学习笔记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

猜你喜欢

转载自blog.csdn.net/twd_1991/article/details/80584722