当丢包或者解码错误导致无法正确解码视频流,或者当一个新的接收者加入到视频通话时,需要一个关键帧来恢复和开始正常解码。关键帧请求机制是确保视频流在不可靠网络环境下能够恢复和维持高质量播放的关键技术之一,mediasoup 支持关键帧请求。
1. 静态结构
关键帧请求要发送到视频源端,因此会在 Producer 汇聚。如果只是简单转发关键帧请求,实现会非常简单。但服务器需要考虑关键帧请求频率限制以及关键帧请求失败处理。其中 KeyFrameReuqestDelayer 用来控制关键帧请求频率,PendingKeyFrameInfo 用来处理关键帧请求重试。
KeyFrameRequestManager 重要方法:
// 普通关键帧请求,要遵守延迟请求规则,请求频率受控制
void KeyFrameNeeded(uint32_t ssrc);
// 强制关键帧请求,立即请求关键帧
void ForceKeyFrameNeeded(uint32_t ssrc);
// 通知收到关键帧报文,做相关处理
void KeyFrameReceived(uint32_t ssrc);
2. 调用流程
Consumer 基于各种原因请求关键帧,请求最终会交给 KeyFrameRequestManager 处理,KeyFrameRequestManager 控制关键帧请求的频率,由 RtpStreamRecv 生成请求关键帧的 RTCP 报文,从关联的 Transport 发送出去。NackGenerator 在必要时也会请求关键帧,但其是直接向 RtpStreamRecv 请求,因此不受 KeyFrameRequestManager 控制。
3. 请求触发
在 mediasoup 中,关键帧请求在以下几个场景中被触发,以确保视频流的连续性和质量:
- 应用请求
应用程序可以通过 mediasoup 信令通道发送特定的命令来请求一个新的关键帧。这通常用于自定义场景下,需要手动触发关键帧请求。
- PLI/FIR 请求
接收端检测到视频流丢帧或者解码错误时,会通过RTCP发送 PLI(Picture Loss Indication)或 FIR(Full Intra Request)消息给发送端。mediasoup 会处理这些 RTCP 请求,并指示发送端生成并发送一个新的关键帧。
- 新用户加入
当新用户加入并成功连接到 mediasoup 的 Transport 时,需要一个关键帧来开始视频解码和显示。mediasoup 会自动触发一个关键帧请求,以确保新用户能够立即看到视频内容。
- SVC 切换分层
在使用 SVC 时,不同的分层提供不同质量和分辨率的视频。当 SvcConsumer 切换到不同的分层时,可能需要一个关键帧来同步和显示新的分层视频内容。mediasoup 会在这种情况下自动触发一个关键帧请求。
- NackGenerator
NackGenerator 在 NACK 请求队列无法腾挪出位置插入新的 NACK 请求时,会清空 NACK 请求队列,并发送关键帧请求。
4. 请求控制
4.1. 频率控制
关键帧请求如果太频繁,会对发送端性能和网络造成影响,有必要对关键帧请求频率进行控制。通常的做法是设置关键帧请求的最小时间间隔。mediasoup 关键帧请求频率控制是通过 KeyFrameRequestDelayer 实现的。
Consumer 在请求关键帧时,KeyFrameRequestManager 会立即发送关键帧请求并创建一个 KeyFrameRequestDelayer 对象,KeyFrameRequestDelayer 对象的生存期设置为关键帧请求时间间隔。如果在 KeyFrameRequestDelayer 对象生存期内,没有其他关键帧请求,KeyFrameRequestDelayer 对象到期后会自动销毁。
KeyFrameRequestDelayer 对象销毁后,一切重新开始,新的关键帧请求会创建新的 KeyFrameRequestDelayer 对象。
如果在 KeyFrameRequestDelayer 对象生存期内,有新的关键帧请求(一个或多个)到来,此时,不会立即触发新的关键帧请求,代之以设置 KeyFrameRequestDelayer 的 keyFrameRequested 标志。KeyFrameRequestDelayer 对象在销毁的时候,会检查 keyFrameRequested 标志,如果置位则会重新启动一次关键帧请求。
强制关键帧请求比较暴力,因为要的急,需要立即发送关键帧请求,不管三七二十一,如果有延迟器,删除重建。
4.2. 超时控制
关键请求如果超时未响应,不能不了了之,需要重新请求,但也不能无限重试。mediasoup 的策略是重新请求一次,如果还是没有响应,那也无能为力了。mediasoup 的关键帧请求的超时控制是通过 PendingKeyFrameInfo 实现的。
KeyFrameRequestManager 发送关键帧请求时,会对应创建一个 PendingKeyFrameInfo 对象。PendingKeyFrameInfo 内部会启动一个定时器,默认时长是 1 秒。如果在 1 倍超时时间内收到关键帧,则会删除此 PendingKeyFrameInfo 对象。
如果在 1 倍超时时间内没有收到关键帧,则会重发关键帧请求,并设置超时后不再重试的标志。如果在 2 倍超时时间内收到关键帧,删除 PendingKeyFrameInfo 对象。
如果在 2 倍超时时间内没有收到关键帧,则删除 PendingKeyFrameInfo 对象,不再重试。
如果在 1 倍超时时间内,有针对此 ssrc 的新的关键帧请求,不会影响超时重试逻辑。
但如果在 2 倍超时时间内,有针对此 ssrc 的新的关键帧请求,则会延长 PendingKeyFrameInfo 对象 1 倍超时时间的生存期。这对后来的关键帧请求比较公平。
5. 重要方法
本文详细讲解了 mediasoup 关键帧请的流程和控制原理,尽量少讲代码,多讲思路和逻辑。对于关键帧请求的场景只是进行了简单总结,没有深入分析,这部分会放到媒体转发章节讲述。mediasoup 实现关键帧请求控制的设计非常巧妙,代码实现小巧精简,是服务器关键帧请求实现的优秀范本,值得大家借鉴。