webrtc jitterbuffer 学习

注:详情可关注微信公众号Deverloper_Taoists

视频通话中的jitterbuffer分析

 

1. 概述

Jitterbuffer在实时通讯中起了重要作用,用于数据接收端,它缓冲了接收到的数据包,在”网络拥塞,定时漂移,路由变更”时,可以在一定程度上让用户感受不到数据波动的影响.Pjsip中的jbuf的功能较为简单,仅支持丢包,适用于网络状况比较好的情况,对于实际的网络状况,客户体验会比较差.故需要移植更好的算法,如webrtc中的jitterbuffer.

 

2. Webrtc的Jitterbuffer分析

2.1主要模块

 

A.video_coding_impl为编解码及数据传输的api层,这里只讨论接受到的数据包的处理,而不关心怎么编码/发送/接收。数据包收到后会通过InsertPacket接口灌入receiver中进行分析处理,最终拼成一个可解的帧,再通过receiverFrameForDecoding接口获取出来送去给解码器解码。

B.codec_database为编解码器的管理(注册,初始化,调用,释放等)。

C.Receiver是jitter_buffer的直接封装,负责jitter_bufferStart(),Flush()(没有看到调Stop(),bug?)

D.Jitterbuffer使用到的模块有framebuffer(维护了decodable_frames_及incomplete_frames_以framebuffer为元素的两个list),jitter_estimator(似乎只和显示延迟有关,且传相关参数至解码器时,并未使用辛苦计算出来的值,直接无视了该参数),inter_frame_delay(与jitter_estimator有关),decoding_state(保存从jitterbuffer取出的用于解码的帧的状况),使用到的类有Packet,encoded_frame。Jitterbuffer一个比较重要的事是维护了一个nack_list,用于存放missing的seq_num,该表会用于retransmite.

Session_info用于包的拼接,根据packet的seq_num对顺序或非顺序增长的包进行排列,根据packets_.markerBit来判断是否一整帧拼完了。如果是重传包,session_info会将该包插入到它应该在的位置上去。

 

2.2主要流程

A.正常模式

网络环境较理想时,jitter buffer调用framebuffer的insertpacket,framebuffer先根据时间戳判断该包是否属于当前帧,判断拼接了该包内存是否超限,未超限的话判断拼接后的buf是否会溢出,若会则重新申请帧内存,然后调用session_info的insertPacket将当前包插入到帧内存相应的位置,这里会根据packets_.front().isFirstPacket && packets_.back().markerBit来判断是否完成拼接一个完整的帧了,如果是完整帧,则decodable_frames_入队(同时清除它在incomplete_frames_的入队),否则判断是否一帧的第一个包,若是,则入队incomplete_frames_。

 

解码时,首先会从decodable_frames_中查询,若有,则取出该帧送去解码,完了后ReleaseFrame。如果decodable_frames_没有,则从incomplete_frames_找,不过即使找到也是不完整的帧或其之前帧不完整,如果这帧完整且是I帧则可解,否则都会出错。

 

B.丢帧模式

使用gtest的用例,举例描述

ASSERT_EQ(VCM_OK, vcm_->SetReceiverRobustnessMode(

      VideoCodingModule::kNone,

      VideoCodingModule::kAllowDecodeErrors));

  InsertPacket(0, 0, true, false, kVideoFrameKey);

  InsertPacket(0, 1, false, false, kVideoFrameKey);

  InsertPacket(0, 2, false, true, kVideoFrameKey);

  EXPECT_EQ(VCM_OK, vcm_->Decode(0));  // Decode timestamp 0.

  EXPECT_EQ(VCM_OK, vcm_->Process());  // Expect no NACK list.

  clock_->AdvanceTimeMilliseconds(33);

  InsertPacket(3000, 3, true, false, kVideoFrameDelta);

  // Packet 4 missing

  InsertPacket(3000, 5, false, true, kVideoFrameDelta);

  EXPECT_EQ(VCM_FRAME_NOT_READY, vcm_->Decode(0));

  EXPECT_EQ(VCM_OK, vcm_->Process());  // Expect no NACK list.

  clock_->AdvanceTimeMilliseconds(33);

  InsertPacket(6000, 6, true, false, kVideoFrameDelta);

  InsertPacket(6000, 7, false, false, kVideoFrameDelta);

  InsertPacket(6000, 8, false, true, kVideoFrameDelta);

  EXPECT_EQ(VCM_OK, vcm_->Decode(0));  // Decode timestamp 3000 incomplete.

  EXPECT_EQ(VCM_OK, vcm_->Process());  // Expect no NACK list.

  clock_->AdvanceTimeMilliseconds(10);

  EXPECT_EQ(VCM_OK, vcm_->Decode(0));  // Decode timestamp 6000 complete.

  EXPECT_EQ(VCM_OK, vcm_->Process());  // Expect no NACK list.

  clock_->AdvanceTimeMilliseconds(23);

  InsertPacket(3000, 4, false, false, kVideoFrameDelta);

  InsertPacket(9000, 9, true, false, kVideoFrameDelta);

  InsertPacket(9000, 10, false, false, kVideoFrameDelta);

  InsertPacket(9000, 11, false, true, kVideoFrameDelta);

  EXPECT_EQ(VCM_OK, vcm_->Decode(0));  // Decode timestamp 9000 complete.

 

这里都是3个包拼成一帧,首先0,1,2号包顺序收到,拼成一帧后被解码。第3号包来后,首先入队incomplete_frames_,然后是第5号包,仅拷贝到了session_info维护的packet队列中。此时调用解码,decodable_frames_为空,incomplete_frames_.size()<=1,会返回没有找到可解帧,没有启动解码。6,7,8号包被插入jitter_buffer,虽然这是一个完整帧,但是其之前的帧是一个未完成的状态,故该帧依旧在incomplete_frames_队列中。此时启动解码,在incomplete_frames_中先找到之前不完整的帧,由于允许出错解码,故解码错但返回正常,时间戳为3000的帧调用ReleaseFrame。再启动解码,找到时间戳为6000的完整帧,若该帧为I帧,则正常解码,否则解码错但返回正常,ReleaseFrame。之后即使4号包再接收到,但由于它所属的帧数据已经被释放掉了,该包被丢弃。

 

C.重传模式

使用gtest的用例,举例描述

  ASSERT_EQ(VCM_OK, vcm_->SetReceiverRobustnessMode(

      VideoCodingModule::kHardNack,

      VideoCodingModule::kNoDecodeErrors));

  InsertPacket(0, 0, true, false, kVideoFrameKey);

  InsertPacket(0, 1, false, false, kVideoFrameKey);

  InsertPacket(0, 2, false, true, kVideoFrameKey);

  clock_->AdvanceTimeMilliseconds(1000 / 30);

  ASSERT_EQ(VCM_OK, vcm_->Decode(0));

  ASSERT_EQ(VCM_FRAME_NOT_READY, vcm_->Decode(0));

  clock_->AdvanceTimeMilliseconds(10);

  ASSERT_EQ(VCM_OK, vcm_->Process());

  ASSERT_EQ(VCM_FRAME_NOT_READY, vcm_->Decode(0));

  InsertPacket(3000, 5 false, true, kVideoFrameDelta);

  clock_->AdvanceTimeMilliseconds(10);

  ASSERT_EQ(VCM_OK, vcm_->Process());

  ASSERT_EQ(VCM_FRAME_NOT_READY, vcm_->Decode(0));

  InsertPacket(3000, 3, true, false, kVideoFrameDelta);

  InsertPacket(3000, 4, false, false, kVideoFrameDelta);

  clock_->AdvanceTimeMilliseconds(10);

  ASSERT_EQ(VCM_OK, vcm_->Process());

  ASSERT_EQ(VCM_OK, vcm_->Decode(0));

 

首先是一个完整帧的3个包被插入jitterbuffer并被取出解码,这时第3,4包漏掉,直接收到了第5包,先将其入队session_info,然后根据传入的seq_num判断是否之前的seq_num+1,若否,则更新missing_sequence_numbers_,将丢失的seq_num(即6,7号)入队。将8号包入队incomplete_frames_。从missing_sequence_numbers_取出信息拷贝至Nacklist,调用Callback函数让发送端根据Nacklist中丢失的seq_num重传包数据,接收端收到后将其插入session_info的正确位置上,并更新missing_sequence_numbers_。最终该帧入队decodable_frames_,并清除其incomplete_frames_的记录。

 

D.双receiver模式

设置主receiver为丢帧模式,次receiver为重传模式,在网络理想的状态下使用主receiver,当发生有包丢失的情况时,激活次receiver,更新nacklist,发送重传请求,并处理重传包。

 

E.其他

1)丢帧

当nacklist过于庞大时,如大于max_nack_list_size_,或当decodable_frames_或incomplete_frames_的元素个数超过阈值kMaxNumberOfFrames,则从最早的帧开始丢,直到碰到一个I帧为止,先丢incomplete_frames_队列,再丢decodable_frames_队列。

2)显示

在codec_databaseGetDecoder中,向解码器注册了一个回调函数,当解码器,如vp8解完一帧,会直接调用该回调函数,应该是用于显示的。

3)jitter_estimator的用处

timing_->SetJitterDelay(jitter_buffer_.EstimatedJitterMs());

  const int64_t now_ms = clock_->TimeInMilliseconds();

  timing_->UpdateCurrentDelay(frame_timestamp);

  next_render_time_ms = timing_->RenderTimeMs(frame_timestamp, now_ms);

……

frame->SetRenderTime(next_render_time_ms);

 

2.3 jitterbuffer主要函数说明

================================

VCMEncodedFrame* VCMJitterBuffer::ExtractAndSetDecode(uint32_t timestamp)

decodable_frames_取出一帧用于解码

更新jitterestimate

当前帧状态更新

当前解码状态更新

丢掉nacklist无用的包

 

================================

int64_t VCMJitterBuffer::LastPacketTime(const VCMEncodedFrame* frame,

                                        bool* retransmitted)

获得最新的包时间戳

 

================================

void VCMJitterBuffer::IncomingRateStatistics(unsigned int* framerate,

                                             unsigned int* bitrate)

输入包帧率及码率统计

 

================================

void VCMJitterBuffer::FrameStatistics(uint32_t* received_delta_frames,

                                      uint32_t* received_key_frames)

输入的关键帧及非关键帧个数统计

 

================================

void VCMJitterBuffer::SetNackMode(VCMNackMode mode,

                                  int low_rtt_nack_threshold_ms,

                                  int high_rtt_nack_threshold_ms)

设置jitterbuffer的健壮性,主要是是否支持nacklist以及重传等待。

 

================================

VCMFrameBufferEnum VCMJitterBuffer::InsertPacket(const VCMPacket& packet,

                                                 bool* retransmitted)

Jitterbuffer的入队管理

 

================================

bool VCMJitterBuffer::NextCompleteTimestamp(

    uint32_t max_wait_time_ms, uint32_t* timestamp)

在设定的等待时间max_wait_time_ms内等待decodable_frames_队列中有一帧可以用于解码

 

================================

bool VCMJitterBuffer::CompleteSequenceWithNextFrame()

如果出现了丢包,那么decodable_frames_会为空,incomplete_frames_会<=1,此时如果有次receiver,需要将一些状态和设置从主receiver拷贝过来,以便次receiver能够继续解码下去。

 

================================

bool VCMJitterBuffer::NextMaybeIncompleteTimestamp(uint32_t* timestamp)

incomplete_frames_队列中找一个完整帧,如果设置成了decode_with_errors_模式,那么可以给出这帧用于解码。

 

================================

uint32_t VCMJitterBuffer::EstimatedJitterMs()

经过一系列复杂的算法,最终预测结果是一个ms值,该值用于配置显示delay。

 

 

3. Pjsip的jbuf介绍

3.1 主要函数功能介绍

层次关系:pjmedia_jbuf模块调用jb_framelist模块

 

================================

PJ_DEF(pj_status_t) pjmedia_jbuf_create(pj_pool_t *pool, /*内存池*/

                    const pj_str_t *name,/*名称*/

unsigned frame_size, /*存放包内容的buf大小*/

unsigned ptime,/*帧间隔,如33ms*/

unsigned max_count,/*缺省为半秒时间内的chunk数(一整帧打包成多个chunk)*/

pjmedia_jbuf **p_jb)/*创建及初始化pjmedia_jbuf数据结构*/

 

A.初始化framelist,这是jbuf核心数据包队列,最大元素个数为max_count,每个元素的buf大小为frame_size

B.framelist对于一个元素的描述是维护了多个属性list,如类型,包内容长度,时间戳等都是一个数组,当修改一个元素的属性时需要操作多个数组。

C.进行一些属性的设置,如丢包算法设置为PJMEDIA_JB_DISCARD_PROGRESSIVE,接收端和发送端的延时超过多少个包就可以丢包等。

D.状态及内部数据复位至缺省或清0。

 

================================

PJ_DEF(pj_status_t) pjmedia_jbuf_destroy(pjmedia_jbuf *jb)

A.没有看到销毁的实际内容,bug?

 

================================

PJ_DEF(void) pjmedia_jbuf_put_frame3(pjmedia_jbuf *jb, /*jbuf数据结构*/

     const void *frame, /*需要入队的包元素*/

     pj_size_t frame_size, /*需要入队的包大小*/

     pj_uint32_t bit_info,/*是否为关键帧标示*/

 

     int frame_seq,/*包序列号*/

     pj_uint32_t ts,/*包时间戳*/

     pj_bool_t *discarded)/*是否是丢包,实际该参数未用*/

A.包入队

B.如果jbuf已满,则移除一些最老的包

C.Jbuf状态刷新

D.Prefetch没有用到

 

================================

PJ_DEF(void) pjmedia_jbuf_peek_frame( pjmedia_jbuf *jb,/*jbuf数据结构*/

      unsigned offset,/*与队列头的相对偏移位置*/

      const void **frame, /*取出的包*/

      pj_size_t *size, /*取出的包大小*/

      char *p_frm_type,/*取出的包类型*/

      pj_uint32_t *bit_info,/*取出的包是否属于关键帧*/

      pj_uint32_t *ts,/*取出的包时间戳*/

      int *seq)

A.从jbuf取出一包用于解码,该函数在解一帧前会多次调用。

B.由于一帧是由多个包组成,解码一帧前会经历两轮查jbuf,第一轮先确认一帧由多少包组成(使用时间戳来判断),第二轮真正的取出包数据。

 

================================

PJ_DEF(unsigned) pjmedia_jbuf_remove_frame(pjmedia_jbuf *jb, ,/*jbuf数据结构*/

                        unsigned frame_cnt),/*一帧的包个数*/

A.解码完一帧数据后,把相应的包从jbuf中清掉。

B.根据队列的排布(有效数据在总队列的中间,及有效数据在总队列的头部及尾部两种情况),用两步清。

C.如果队列中有discard帧,也相应清掉。

 

 

================================

PJ_INLINE(void) jbuf_update(pjmedia_jbuf *jb, /*jbuf数据结构*/

                                   int oper)/*入队or出队标示*/

A.更新jbuf,主要是丢包算法需要的一些变量的更新,目前只在put_frame时更新(没有调到get_frame接口,peek_frame中没有更新jbuf)

B.执行丢帧

 

================================

static void jbuf_discard_static(pjmedia_jbuf *jb)

1.每更新一次jbuf丢一包

 

================================

static void jbuf_discard_progressive(pjmedia_jbuf *jb)

A.每更新一次jbuf丢多包

 

链表操作函数(如下),不再赘述

jb_framelist_init

jb_framelist_destroy

jb_framelist_reset

jb_framelist_get

jb_framelist_peek

jb_framelist_remove_head

jb_framelist_put_at

jb_framelist_discard

 

3.2 基本流程

发送端和接收端的代码在一起

A. 创建:

PJ_DEF(pj_status_t) pjmedia_vid_stream_create(

pjmedia_endpt *endpt,

pj_pool_t *pool,

pjmedia_vid_stream_info *info,

pjmedia_transport *tp,

void *user_data,

pjmedia_vid_stream **p_stream)

{

……

//创建解码数据通道

status = create_channel( pool, stream, PJMEDIA_DIR_DECODING,

     info->rx_pt, info, &stream->dec);

//创建编码数据通道

status = create_channel( pool, stream, PJMEDIA_DIR_ENCODING,

     info->tx_pt, info, &stream->enc);

 

……

/* Create jitter buffer */

    status = pjmedia_jbuf_create(pool, &stream->dec->port.info.name,

                                 PJMEDIA_MAX_MRU,

 1000 * vfd_enc->fps.denum / vfd_enc->fps.num,

 jb_max, &stream->jb);

……

//接收端数据处理对接

//(pjmedia_transport_attach对应Transport_udp.c的函数transport_attach)

status = pjmedia_transport_attach(tp, stream, &info->rem_addr,

      &info->rem_rtcp,

      pj_sockaddr_get_len(&info->rem_addr),

                                      &on_rx_rtp, &on_rx_rtcp);

……

}

 

B. 数据流转

PJ_DEF(pj_status_t) pjmedia_vid_port_create( pj_pool_t *pool,

     const pjmedia_vid_port_param *prm,

     pjmedia_vid_port **p_vid_port)

{

……

//注册编解码定时器

status = pjmedia_clock_create2(pool, ¶m,

                                       PJMEDIA_CLOCK_NO_HIGHEST_PRIO,

                                       (vp->dir & PJMEDIA_DIR_ENCODING) ?

                                       &enc_clock_cb: &dec_clock_cb,

                                       vp, &vp->clock);

……

}

 

C . 编码

static void enc_clock_cb(const pj_timestamp *ts, void *user_data)

{

……

status = pjmedia_port_put_frame(vp->client_port, &frame_);

}

 

PJ_DEF(pj_status_t) pjmedia_port_put_frame( pjmedia_port *port,

    pjmedia_frame *frame )

{

    PJ_ASSERT_RETURN(port && frame, PJ_EINVAL);

 

    if (port->put_frame)

return port->put_frame(port, frame);

    else

return PJ_EINVALIDOP;

}

 

而这个port是在之前的create_channel中赋值

static pj_status_t create_channel( pj_pool_t *pool,

   pjmedia_vid_stream *stream,

   pjmedia_dir dir,

   unsigned pt,

   const pjmedia_vid_stream_info *info,

   pjmedia_vid_channel **p_channel)

{

……

channel->port.put_frame = &put_frame;

……

}

 

static pj_status_t put_frame(pjmedia_port *port,

                             pjmedia_frame *frame)

{    

……

    /* Loop while we have frame to send */

    for (;;) {

status = pjmedia_rtp_encode_rtp(&channel->rtp,

                                channel->pt,

                                (has_more_data == PJ_FALSE ? 1 : 0),

                                frame_out.size,

                                rtp_ts_len,

                                (const void**)&rtphdr,

                                &rtphdrlen);

// Send the RTP packet to the transport.

status = pjmedia_transport_send_rtp(stream->transport,

                                    (char*)channel->buf,

                                    frame_out.size +

sizeof(pjmedia_rtp_hdr));

 

/* Encode more! */

//将编码后帧打成多个包

status = pjmedia_vid_codec_encode_more(stream->codec,

                                       channel->buf_size -

   sizeof(pjmedia_rtp_hdr),

               &frame_out,

       &has_more_data);

……

    return PJ_SUCCESS;

}

 

D. 数据接收

static void on_rx_rtp( void *data,

       void *pkt,

                       pj_ssize_t bytes_read)

 

{

status = pjmedia_rtp_decode_rtp(&channel->rtp, pkt, bytes_read,

    &hdr, &payload, &payloadlen);

……

//数据入jbuf队列

pjmedia_jbuf_put_frame3(stream->jb, payload, payloadlen, 0,

pj_ntohs(hdr->seq), pj_ntohl(hdr->ts), NULL);

 

}

 

 

E. 解码

static void dec_clock_cb(const pj_timestamp *ts, void *user_data)

{

……

//

    status = vidstream_render_cb(vp->strm, vp, &frame);

    if (status != PJ_SUCCESS)

        return;

    //送显

    if (frame.size > 0)

status = pjmedia_vid_dev_stream_put_frame(vp->strm, &frame);

}

static pj_status_t vidstream_render_cb(pjmedia_vid_dev_stream *stream,

       void *user_data,

       pjmedia_frame *frame)

{

……

status = pjmedia_port_get_frame(vp->client_port, vp->frm_buf);

……

}

 

PJ_DEF(pj_status_t) pjmedia_port_get_frame( pjmedia_port *port,

    pjmedia_frame *frame )

{

    PJ_ASSERT_RETURN(port && frame, PJ_EINVAL);

 

    if (port->get_frame)

return port->get_frame(port, frame);

    else {

frame->type = PJMEDIA_FRAME_TYPE_NONE;

return PJ_EINVALIDOP;

    }

}

 

static pj_status_t get_frame(pjmedia_port *port,

                             pjmedia_frame *frame)

{

……

decode_frame(stream, frame)

}

static pj_status_t decode_frame(pjmedia_vid_stream *stream,

                                pjmedia_frame *frame)

{

//从jbuf中取包

pjmedia_jbuf_peek_frame(stream->jb, cnt, NULL, NULL,

&ptype, NULL, &ts, &seq);

/* Decode */

//包拼接及解码

status = pjmedia_vid_codec_decode(stream->codec, cnt,

                                  stream->rx_frames,

                                  frame->size, frame);

//从jbuf删除已解码完的包们

pjmedia_jbuf_remove_frame(stream->jb, cnt);

}

 

3.3 健壮性

只设置了丢帧这种处理方式,没有请求重传.从代码上看,两种丢帧方式都没有关注丢掉的包对于一个完整帧的影响,即没有根据时间戳,bit_info(是否关键帧)及marker_bit(一帧的结束包为1,其他为0)做判断来丢掉一整帧或丢掉所有非关键帧,导致的结果是传给解码的数据可能是缺失的。(待确认)

 

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值