专注webrtc、kurento音视频开发
qq:911921258
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。
专注webrtc、kurento 音视频通话
qq :911921258