备注: 基于 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
......
//两道流的ssrc
a=ssrc-group:SIM 687580381 1424746405
// ssrc 与 nack ssrc
a=ssrc-group:FID 687580381 3320499586
a=ssrc-group:FID 1424746405 2590647556
a=ssrc:687580381 cname:uC7imfOfwk+q50+I
a=ssrc:687580381 msid:1079508994 video-default
a=ssrc:687580381 mslabel:1079508994
a=ssrc:687580381 label:video-default
a=ssrc:1424746405 cname:uC7imfOfwk+q50+I
a=ssrc:1424746405 msid:1079508994 video-default
a=ssrc:1424746405 mslabel:1079508994
a=ssrc:1424746405 label:video-default
a=ssrc:3320499586 cname:uC7imfOfwk+q50+I
a=ssrc:3320499586 msid:1079508994 video-default
a=ssrc:3320499586 mslabel:1079508994
a=ssrc:3320499586 label:video-default
a=ssrc:2590647556 cname:uC7imfOfwk+q50+I
a=ssrc:2590647556 msid:1079508994 video-default
a=ssrc:2590647556 mslabel:1079508994
a=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 做成产品级的功能需要解决的问题,
如何设计服务器转发策略.
同时开启分辨率自适应功能, 发送端,接收端与服务端如何做信息同步.
评论