注:详情可关注微信公众号Deverloper_Taoists
1. 概述
Jitterbuffer在实时通讯中起了重要作用,用于数据接收端,它缓冲了接收到的数据包,在”网络拥塞,定时漂移,路由变更”时,可以在一定程度上让用户感受不到数据波动的影响.Pjsip中的jbuf的功能较为简单,仅支持丢包,适用于网络状况比较好的情况,对于实际的网络状况,客户体验会比较差.故需要移植更好的算法,如webrtc中的jitterbuffer.
2. Webrtc的Jitterbuffer分析
2.1主要模块
A.video_coding_impl为编解码及数据传输的api层,这里只讨论接受到的数据包的处理,而不关心怎么编码/发送/接收。数据包收到后会通过InsertPacket接口灌入receiver中进行分析处理,最终拼成一个可解的帧,再通过receiver的FrameForDecoding接口获取出来送去给解码器解码。
B.codec_database为编解码器的管理(注册,初始化,调用,释放等)。
C.Receiver是jitter_buffer的直接封装,负责jitter_buffer的Start(),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_database的GetDecoder中,向解码器注册了一个回调函数,当解码器,如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)做判断来丢掉一整帧或丢掉所有非关键帧,导致的结果是传给解码的数据可能是缺失的。(待确认)