注:本文已收录专利,版权所有。另外,部分段落缩减了部分内容。
1. 背景
当下移动互联网视频直播正处于如火如荼的井喷式发展当中,不同的行业(比如教育、医疗、旅游等)都涉足参与进来,企图在这个市场上占有一席之地,争当独角兽式的公司。传统的直播,大多数是单向型,比如电视台或者运营商直播,用户只需要打开终端收看即可,对于实时性并没有太大的要求。而移动直播往往在功能上需要主播端和播放端有交互,这种交互不限于文字的互动,视频与语音的互动也日趋成为基本的交互场景需求,传统的做法是主播端采集视频数据并编码成x264、采集音频数据并编码成aac,再合并打包后通过一定的Qos算法将音视频流数据通过rtmp协议(基于TCP协议之上)推流到CDN服务器进行分发,用户端从CDN服务器拉流解码来播放,这种方式的延时表现在rtmp推流到CDN和播放器从CDN通过rtmp拉流缓存播放,整个网络链路的延迟通常在1-3秒或者更差。在网络不稳定的情况下,通常提高用户观看体验的方式是通过在主播端和播放端设置Gop缓存,让码率均匀;另外可以在主播端通过Qos算法检测变化的网络来动态改变码率和帧率;还可以接入多个视频云服务CDN提供商,这样可以做推拉流线路互备,对推流后视频服务集群再优化并根据端点网络状况做实时线路切换。另外一种方式是通过http HLS的方式进行传输,这种切片式的直播方式延迟更大,不适合高互动式的场景直播。
所以本文提出了一种基于UDP方式的视频传输方案(webrtc层外),旨在服务于低延迟高实时互动式的直播场景,这种方式能将延迟控制在800ms人眼可接收范围内,在网络带宽比较差或者强丢包、乱序的情况下,通过缓存机制、带宽自适应检测机制实时汇报给编码器降低码率、分辨率、帧率,能可靠有效的将实时画面完整的播放出来,提高了用户体验。
2 视频传输流程
2.1 视频传输时序图
本方案采用基于udp协议的报文传输,设计了一套私有的报文格式,以使得整个报文在网络链路中传输简单可靠并可控,相对于TCP传输的方式,大大的降低了报文的大小与复杂度,保证了整个传输过程的实时性。如下图1为视频传输的时序图。
2.2 关键技术
本方案视频编码采用h264的形式(也可以是h265,负载数据是不受传输模块限制的,可以在两端进行适配和协调),因为B帧是双向预测帧,它需要根据后向视频帧来预测编码,一定程度上会增大编解码延迟,所以为了保证传输和播放的实时性,本方案视频压缩丢弃B帧编码。
2.2.1 发送端处理
2.2.1.1 视频切片算法
当编码器编码出一帧完整的h264视频帧数据时送入发送端,对于高分辨率的视频编码帧,帧大小往往高于UDP网络MTU,所以此时需要发送端对其进行分片处理再发送,每次按照分片单位来发送帧块数据。
视频编码帧的最大分片数SMAX为500,规定单个分片字节大小SEGS为800bytes (根据探测MTU可动态计算调整)。当帧字节大小FS小于SEGS+50时只分为一个分片;否则,整数倍的分片数S为FS/SEGS,超出的最后字节数FS1为FS%SEGS,如果FS1大于50,则再单独分一个分片,此时总分片数为S+1,否则如果FS1小于50并且大于0时,将FS1字节累加放入最后一个分片中,此时总分片数为S。如下图2为视频分片结构与流程。
2.2.1.2 发送端滑动窗口
发送缓存区保存着所有正在发送且没有收到接收方连续seq确认acked的报文。当收到peer端发来的分片的ack信息(携带一系列丢包的分片seq和已经ack并连续处理到的seq)时,发送端从发送缓存中获取对应的那些丢包seq并重发分片(如下图的seq 10/12分片),同时从缓存中删除区间[s1+1, s2]中的分片,同时移动滑动窗口。如下图3为发送端滑动窗口滑动过程图。
2.2.1.3 发送端带宽自适应调整算法
对于网络的不可预测性,可能出现抖动、拥塞或者很多的丢包,如果按照固定的码率和参数来发送视频帧,这会导致发送端与接收端的线路间更拥塞,从而使观看端出现更多的播放延迟或者马赛克。所以在发送端需要做带宽的实时估算来探测网络情况,从而便于实时根据网络带宽来调整上层视频编码器的帧率或者码率。
在发送端设置了一个定时器,每10秒钟(可配置)做一次带宽统计。rtt修正值(网络抖动的时间差值)为rtt_var,期望目标带宽为dst_bw,当前发送的帧分片时间为cur_ts,最后acked的帧分片时间为acked_ts,则
delay_ts_delta = cur_ts–acked_ts
当前单位时间内的acked带宽为bw,则带宽抖动修正值acked_bw为:
acked_bw = (acked_bw * 3 + bw) / 4
如果当前有包在重发,且 delay _ts_delta 大于 8 * MAX(rtt + rtt_var, 100),则dst_bw = acked_bw,向下降低调整视频编码器的分辨率、帧率或码率来保证播放的实时性和流畅性;否则如果acked_bw 大于0,则dst_bw = acked_bw * 9 / 8,向上提高调整视频编码器的分辨率、帧率或码率来恢复清晰度和提高播放体验。
2.2.1.4 过期帧丢弃策略
在网络拥塞时可能发送窗口缓冲区中有很多正在发送中的分片报文,为了缓解拥塞和减少延迟会对整个缓冲区做检查,如果有超过一定阈值时间的GOP 帧存在,则会将这个 GOP 内的所有帧的分片从窗口缓冲区移除,并将它的下一个 GOP 的 I 帧 fid和分片seq 通过 syn 协议同步到各个接收端上,接收端接收到此协议,会将最新连续 seq 设置成同步过来的 seq。如果频繁出现过期帧丢弃处理则会造成一定程度上的播放卡顿,此时说明当前网络不适合传输高分辨率或高帧率的视频,可以通知上层视频编码器设置为更小的分辨率或帧率。
2.2.2 接收端处理
2.2.2.1 接收端收包处理
接收端收到服务器中转过来的syn消息(携带用户uid、开始分片序列号start_seq、帧率)后,根据uid查找(或分配)对应的发送者并激活,同时根据start_seq更新已经连续接收到的分片序列号base_seq和当前接收到的分片最大序列号max_seq。
接收端第一帧必须是关键帧(即一个完整GOP的开始),如果是其他帧则丢弃,直到出现关键帧为止,因为如果第一帧是P帧将出现花屏现象。
每收到一个分片,如果此分片的seq小于base_seq或者帧fid小于已经接收到的最小帧min_fid(已经接收过了)或者分片的seq大于max_seq+2000(太大的跳变导致丢包缓存太大),则丢弃;否则,当收到第一个关键帧的第一个片段时,记下此时max_seq和base_seq,将此分片放入对应帧fid的分片缓存区,同时计算单位时间内的帧间隔时长,并更新丢包缓存表,如果此分片的seq和前一个已接收分片的seq连续,则更新base_seq为此分片的seq,再更新max_seq为MAX(max_seq, seq),最后发送ack给peer端。
如下图5为接收端收到帧分片并存储到缓存区的一个实例,对于一个完整的帧fid1的分片序列为区间seq [1, 8],帧fid2的分片序列为区间seq [9, 20]。帧的分片seq总是单调递增的。
2.2.2.2 更新丢包缓存区策略
如下图所示,接收端已经连续收到了seq [1, 5] 的分片包,此时base_seq 和 max_seq 都是5,当接收到下一个分片包seq 10时,将seq 10从丢包缓存中删除,此时认为 seq [6, 9] 是暂时丢失的(可能乱序不一定真丢失,需要后续的包来确认),如果丢包缓存中没有此序号的丢包,则将它们放入丢包缓存中,同时更新它们的丢包时间戳(当前时间减去rtt值),等待下一次的接收确认。
2.2.2.3 接收端发送回应ack策略
当接收端每次收到peer端发来的分片,需要判断是否发送回应ack给peer端,发送周期是10ms(毫秒),小于10ms则不发送ack,发送太频繁会导致网络拥塞;否则,获取接收缓存中最老的一帧中最小的分片包序号min_seq,检查接收端丢包缓存区,并删除区间[base_seq+1, min_seq]中的丢包,表明这些丢包已经处理过了,同时设置滑动窗口的base_seq为min_seq,然后循环扫描丢包缓存区检查当前时间是否超过一个发送分片的rtt时间,如果超过则累加此分片的丢包计数器,并更新丢包分片的时间为当前时间,最后将所有超过rtt时长的丢包发送ack回peer端,最后计算播放缓存延迟时长。
在帧缓存中会选择性删除一些播放过的帧分片,播放过的帧分片是不需要进行重发的。
在接收端设置了一个定时器,每隔5ms(毫秒)也会检测一次是否发送回应ack给peer端,并扫描检查和更新丢包缓存区。
2.2.3 获取播放视频帧处理
2.2.3.1 播放缓存区策略
在当前播放端设置了一个帧缓存区,如果缓存区过大时播放延迟就大,过小时又会出现播放卡顿情况。所以设置播放缓存区策略就至关重要,缓冲时间大小wait_ts应该大于rtt + 2 * rtt_val,根据重发报文的次数来决定,在接收端的计时器中定期检查丢包数和rtt时长来动态的确定wait_ts的大小。
每次上层从播放缓存区获取一帧时,内部都会检查当前播放缓存区是处于可播放(max_fid>min_fid并且缓存区中最新帧(max_fid)的时间戳max_ts>wait_ts * 5 / 4,则更新可播放的绝对时间戳play_ts为当前系统时间且cached_ts = max_ts - wait_ts * 5 / 4)中还是缓冲(max_fid=min_fid)中,如果在缓冲中则返回空数据;否则同步更新播放缓冲帧时间戳,如果缓存中最久的一帧(min_fid)的所有分片已经接收完整并且此帧时间F_Oldest_ts在播放缓存时间cached_ts内,则合并当前帧分片返回给上层解码播放,同时从缓存中删除此帧,并且更新min_fid为此帧的fid和当前已经缓存到的帧的时间戳cached_ts。
此cached_ts的计算方式为:当 F_Oldest_ts + wait_ts * 5 / 4 >= max_ts(说明缓存区帧很少)或者 min_fid + 1 = max_fid (说明只剩一帧)时,cached_ts = F_Oldest_ts;否则cached_ts = max_ts - wait_ts * 5 / 4,说明缓存区的帧足够。