备注: 基于 webrtc89 版本, codec 选择的是 H264!!!
一,概述
Simulcast is a technique by which a WebRTC client encodes the same video stream twice in different resolutions and bitrates and sending these to a router who then decides who receives which of the streams.
上文摘自webrtcglossary,目前 webrtc 已经支持 h264 Simulcast 功能,发送端将一路视频源编码成多道不同分辨率的流并发送到服务器,这样接收端就可以根据需要订阅不同的流,好处是可以节省了服务器转码的消耗资源,但是当发送端资源受限,多道流势必会相互抢占资源,影响视频的质量.
二,simulcast 开启
webrtc 89 版本开启 simulcast 相对简单,仅需在 createoffer 设置 RTCOfferAnswerOptions num_simulcast_layers 参数就可以了,示例:
const int kMaxVideoSimucastLayersNum = 2;int RTCPeerConnection::createOffer(){ webrtc::PeerConnectionInterface::RTCOfferAnswerOptions option; option.num_simulcast_layers = kMaxVideoSimucastLayersNum; mWebrtcPeerConnection->CreateOffer(csdObserver, option);}
复制代码
当调用 peerconnection 开始创建 offer,根据 num_simulcast_layers 的值生成对应个数的 ssrc,看下 simulcast sdp 的例子:
v=0......//两道流的ssrca=ssrc-group:SIM 687580381 1424746405 // ssrc 与 nack ssrca=ssrc-group:FID 687580381 3320499586 a=ssrc-group:FID 1424746405 2590647556a=ssrc:687580381 cname:uC7imfOfwk+q50+Ia=ssrc:687580381 msid:1079508994 video-defaulta=ssrc:687580381 mslabel:1079508994a=ssrc:687580381 label:video-defaulta=ssrc:1424746405 cname:uC7imfOfwk+q50+Ia=ssrc:1424746405 msid:1079508994 video-defaulta=ssrc:1424746405 mslabel:1079508994a=ssrc:1424746405 label:video-defaulta=ssrc:3320499586 cname:uC7imfOfwk+q50+Ia=ssrc:3320499586 msid:1079508994 video-defaulta=ssrc:3320499586 mslabel:1079508994a=ssrc:3320499586 label:video-defaulta=ssrc:2590647556 cname:uC7imfOfwk+q50+Ia=ssrc:2590647556 msid:1079508994 video-defaulta=ssrc:2590647556 mslabel:1079508994a=ssrc:2590647556 label:video-default
复制代码
三,编码器开启 simulcast 功能
当 offer 创建成功,其实编码器其实还是不知道这些信息的,需要调用 SetLocalDescription 把相关的参数设置到编码器的,流程如下
调用 SetLocalDescription, offer 中的信息被提取,填充 WebRtcVideoSendStream 成员 parameters_,rtp_parameters_,看下 WebRtcVideoSendStream 的声明:
class WebRtcVideoSendStream{ pubic: webrtc::VideoEncoderConfig CreateVideoEncoderConfig(const VideoCodec& codec) const; private: VideoSendStreamParameters parameters_; webrtc::RtpParameters rtp_parameters_; webrtc::VideoSendStream* stream_; };
复制代码
收到远端 answer 消息时,调用 SetRemoteDescription,提取 answer ,然后再结合第一步 offer 中的信息由 VideoStreamEncoder 创建 VideoEncoderConfig 对象 .
完成 1,2 两步后,视频数据已经开始采集了,当首帧数据上来 VideoStreamEncoder 会被缓存到 last_frame_info_。
创建 layers,这一步的主要作用是根据 VideoEncoderConfig 和 last_frame_info_生成 layers,并填充各个 layer 的 width,height,max_qp,max_bitrate_bps,target_bitrate_bps,min_bitrate_bps 信息,为了后面的带宽分配做准备,layer 的 width/height 是根据 last_frame_info_的宽高/2 递减,其中 max_bitrate_bps,target_bitrate_bps,min_bitrate_bps 是查表所得,见表 kSimulcastFormats,
constexpr const SimulcastFormat kSimulcastFormats[] = { {1920, 1080, 3, webrtc::DataRate::KilobitsPerSec(5000), webrtc::DataRate::KilobitsPerSec(4000), webrtc::DataRate::KilobitsPerSec(800)}, {1280, 720, 3, webrtc::DataRate::KilobitsPerSec(2500), webrtc::DataRate::KilobitsPerSec(2500), webrtc::DataRate::KilobitsPerSec(600)}, {960, 540, 3, webrtc::DataRate::KilobitsPerSec(1200), webrtc::DataRate::KilobitsPerSec(1200), webrtc::DataRate::KilobitsPerSec(350)}, {640, 360, 2, webrtc::DataRate::KilobitsPerSec(700), webrtc::DataRate::KilobitsPerSec(500), webrtc::DataRate::KilobitsPerSec(150)}, {480, 270, 2, webrtc::DataRate::KilobitsPerSec(450), webrtc::DataRate::KilobitsPerSec(350), webrtc::DataRate::KilobitsPerSec(150)}, {320, 180, 1, webrtc::DataRate::KilobitsPerSec(200), webrtc::DataRate::KilobitsPerSec(150), webrtc::DataRate::KilobitsPerSec(30)}, {0, 0, 1, webrtc::DataRate::KilobitsPerSec(200), webrtc::DataRate::KilobitsPerSec(150), webrtc::DataRate::KilobitsPerSec(30)}};
复制代码
用 layers 填充 codec,这一步没有什么好说的,layers 转换成 codec 对象。
设置编码器
4,5,6 的实现在 VideoStreamEncoder::ReconfigureEncoder
VideoStreamEncoder::ReconfigureEncoder(){ //1,创建layers std::vector<VideoStream> streams = encoder_config_.video_stream_factory->CreateEncoderStreams( last_frame_info_->width, last_frame_info_->height, encoder_config_); //2,填充codec VideoCodec codec; if (!VideoCodecInitializer::SetupCodec(encoder_config_, streams, &codec)) { } //3,设置到编码器 if (encoder_->InitEncode( &send_codec_, VideoEncoder::Settings(settings_.capabilities, number_of_cores_, max_data_payload_length)) != 0) }
复制代码
经过上述的步骤,编码器可能已经有视频流已经出来,但是有可能没有实现我们想要的功能,这因为当总带宽不足时,webrtc 只会编出小流。这涉及了 simulast 的带宽分配相关,见第四节。
四,simulast 带宽分配
上图是新评估出的带宽如何设置到编码器的流程图,其实当这个带宽值传递到 VideoStreamEncoder 之前,已经减去了 fec,nack 和视频封包所占用带宽,具体处理逻辑在 RtpVideoSender::OnBitrateUpdated 中。
第三节创建 layers 时,每个 layer max_bitrate_bps,target_bitrate_bps,min_bitrate_bps 都会根据 kSimulcastFormats 填充一个默认值,下面讲下 gcc 评估出的带宽分配给每个 simulcast 的 layer 实现逻辑。核心函数有两个,我们结合代码直接分析。
void SimulcastRateAllocator::DistributeAllocationToSimulcastLayers( DataRate total_bitrate, DataRate stable_bitrate, VideoBitrateAllocation* allocated_bitrates) { DataRate left_in_total_allocation = total_bitrate; DataRate left_in_stable_allocation = stable_bitrate; //1, 按照每个layer 默认的maxBitrate 进行从小到大排序 std::vector<size_t> layer_index(codec_.numberOfSimulcastStreams); std::iota(layer_index.begin(), layer_index.end(), 0); std::stable_sort(layer_index.begin(), layer_index.end(), [this](size_t a, size_t b) { return std::tie(codec_.simulcastStream[a].maxBitrate) < std::tie(codec_.simulcastStream[b].maxBitrate); });
...... //2, 开始为每个layer 分配带宽 for (; active_layer < codec_.numberOfSimulcastStreams; ++active_layer) { // 3, 获取每个layer 对象 const SpatialLayer& stream = codec_.simulcastStream[layer_index[active_layer]]; DataRate min_bitrate = DataRate::KilobitsPerSec(stream.minBitrate); DataRate target_bitrate = DataRate::KilobitsPerSec(stream.targetBitrate); ...... // 4, 判断剩余带宽是否满足当前layer 最小需求,如何不满足,编码器则不会编出对应的流, // 这就是上面我提到的,为什么只会编出小流的原因。 if (left_in_stable_allocation < min_bitrate) { allocated_bitrates->set_bw_limited(true); break; }
top_active_layer = layer_index[active_layer]; stream_enabled_[layer_index[active_layer]] = true; DataRate layer_rate = std::min(left_in_total_allocation, target_bitrate); // 5, 设置目标码率 allocated_bitrates->SetBitrate(layer_index[active_layer], 0, layer_rate.bps()); ...... // 6, 计算剩余带宽 left_in_total_allocation -= layer_rate; left_in_stable_allocation -= std::min(left_in_stable_allocation, target_bitrate); }
// 7,继续分配剩余带宽,逻辑简单不做解释 if (left_in_total_allocation > DataRate::Zero()) { const SpatialLayer& stream = codec_.simulcastStream[top_active_layer]; DataRate initial_layer_rate = DataRate::BitsPerSec( allocated_bitrates->GetSpatialLayerSum(top_active_layer)); DataRate additional_allocation = std::min( left_in_total_allocation, DataRate::KilobitsPerSec(stream.maxBitrate) - initial_layer_rate); allocated_bitrates->SetBitrate( top_active_layer, 0, (initial_layer_rate + additional_allocation).bps()); }}
复制代码
simulcast 每层流都是独立的, 这样又可以按照 Temporal Scalability 继续分多层, 这个函数的功能就是为每个 Temporal 层按照下面的规则继续分配带宽.
static const float kLayerRateAllocation[kMaxTemporalStreams][kMaxTemporalStreams] = { {1.0f, 1.0f, 1.0f, 1.0f}, // 1 layer {0.6f, 1.0f, 1.0f, 1.0f}, // 2 layers {60%, 40%} {0.4f, 0.6f, 1.0f, 1.0f}, // 3 layers {40%, 20%, 40%} {0.25f, 0.4f, 0.6f, 1.0f} // 4 layers {25%, 15%, 20%, 40%}};
复制代码
五, Simulcast 的 layer 分辨率设置
上面提到 webrtc 创建 layers 时,layer 的宽高/2 递减,当不能满足我们的的需求时,可能就需要应用层主动来做调整了,具体的流程,
1,获取到 VideoRtpSender 对象,该类最终会从 WebRtcVideoSendStream 取出 rtp_parameters_,我们可以设置视频下调比例(scale_resolution_down_by),带宽分配优先级(bitrate_priority),最大码率等各种信息。
2,根据需要填充该结构体,代码示例:
//rtpParameters 其实就是上面6.1 rtp_parameters_ webrtc::RtpParameters rtpParameters = mVideoSenderMap[tag].sender->GetParameters(); webrtc::RtpEncodingParameters encodingParameters; rtpParameters.encodings.push_back(encodingParameters); // update rtpParameters ....... mVideoSenderMap[tag].sender->SetParameters(rtpParameters);
复制代码
六,禁止 simulcast 层数变化
simulcast 层数会随着视频的分辨率调整而改变,具体有以下几种,
当 simulcast 层数不断发生变化,势必会引起 server 端转发策略设计的复杂性,我们可以通过在 webrtc::field_trial 添加"WebRTC-LegacySimulcastLayerLimit/Disabled"就可以把它关闭,见函数:
size_t LimitSimulcastLayerCount(int width, int height, size_t need_layers, size_t layer_count, const webrtc::WebRtcKeyValueConfig& trials) { if (!absl::StartsWith(trials.Lookup(kUseLegacySimulcastLayerLimitFieldTrial), "Disabled")) { size_t adaptive_layer_count = std::max( need_layers, kSimulcastFormats[FindSimulcastFormatIndex(width, height)].max_layers); if (layer_count > adaptive_layer_count) { layer_count = adaptive_layer_count; } } return layer_count;}
复制代码
七,simulcast 做成产品级功能的思考
webrtc 现在确实支持 h264 simulcast 功能,但是现在主流都是些 SFU 架构, 我们不仅要考虑发送端分辨率自适应的情况,还要考虑接受端的情况, 总结下把 simulcast 做成产品级的功能需要解决的问题,
如何设计服务器转发策略.
同时开启分辨率自适应功能, 发送端,接收端与服务端如何做信息同步.
评论