前面提到视频编码器会根据图像的前后变化进行选择性压缩,因为刚开始接收端是没有收到任何图像,那么编码器在开始压缩的视频时需要做个全量压缩,这个全量压缩在 H.264 中 I 帧,后面的视频图像根据这个I帧来做增量压缩,这些增量压缩帧叫做 P 帧,H.264 为了防止丢包和减小带宽还引入一种双向预测编码的 B 帧,B 帧以前面的 I 或 P 帧和后面的 P 帧为参考帧。H.264 为了防止中间 P 帧丢失视频图像会一直错误它引入分组序列(GOP)编码,也就是隔一段时间发一个全量 I 帧,上一个 I 帧与下一个 I 帧之间为一个分组 GOP。
这种现象不是我们想看到的。为了避免这类问题的发生,一般如果发现 P 帧或者 I 帧丢失,就不显示本 GOP 内的所有帧,直到下一个 I 帧来后重新刷新图像。但是 I 帧是按照帧周期来的,需要一个比较长的时间周期,如果在下一个 I 帧来之前不显示后来的图像,那么视频就静止不动了,这就是出现了所谓的卡顿现象。如果连续丢失的视频帧太多造成解码器无帧可解,也会造成严重的卡顿现象。视频解码端的卡顿现象和马赛克现象都是因为丢帧引起的,最好的办法就是让帧尽量不丢。
知道 H.264 的原理和分组编码技术后所谓的秒开技术就比较简单了,只要发送方从最近一个 GOP 的 I 帧开发发送给接收方,接收方就可以正常解码完成的图像并立即显示。但这会在视频连接开始的时候多发一些帧数据造成播放延迟,只要在接收端播放的时候尽量让过期的帧数据只解码不显示,直到当前视频帧在播放时间范围之内即可。
报文冗余:
报文冗余很好理解,就是一个报文在发送的时候发送 2 次或者多次。这个做的好处是简单而且延迟小,坏处就是需要额外 N 倍(N 取决于发送的次数)的带宽。
FEC:
Forward Error Correction,即向前纠错算法,常用的算法有纠删码技术(EC),在分布式存储系统中比较常见。最简单的就是 A B 两个报文进行 XOR(与或操作)得到 C,同时把这三个报文发往接收端,如果接收端只收到 AC,通过 A 和 C 的 XOR 操作就可以得到 B 操作。这种方法相对增加的额外带宽比较小,也能防止一定的丢包,延迟也比较小,通常用于实时语音传输上。对于 1080P 300KB/S 码率的超清晰视频,哪怕是增加 20% 的额外带宽都是不可接受的,所以视频传输不太建议采用 FEC 机制。
接入协商阶段:
主要是发送端发起一个视频传输接入请求,携带本地的视频的当前状态、起始帧序号、时间戳和 MTU 大小等,接收方在收到这个请求后,根据请求中视频信息初始化本地的接收通道,并对本地 MTU 和发送端 MTU 进行比较取两者中较小的回送给发送方, 让发送方按协商后的 MTU 来分片。示意图如下:
帧分片:
前面我们提到 MTU 和视频帧大小,在 1080P 下大部分视频帧的大小都大于 UDP 的 MTU 大小,那么就需要对帧进行分片,分片的方法很简单,按照先连接过程协商后的 MTU 大小来确定分片大小(确定分片大小的算法在 MTU 小节已经介绍过),然后将 帧数据按照分片大小切分成若干份,每一份分片以 segment 报文形式发往接收方。
重传:
重传比较简单,我们采用 pull 方式来实现重传,当接收方发生丢包,如果丢包的时刻 T1 + rtt_var< 接收方当前的时刻 T2,就认为是丢包了,这个时候就会把所有满足这个条件丢失的报文 ID 构建一个 segment ack 反馈给发送方,发送方收到这个反馈根据 ID 到重发窗口缓冲区中查找对应的报文重发即可。