參考:
https://www.cnblogs.com/ishen/p/15333271.html
https://zhuanlan.zhihu.com/p/603421239
1. 生成
1.1 等待並籌齊多個原始包
webrtc 會等待籌齊多個 rtp 包後,再統一生成冗餘包,參看 UlpfecGenerator::AddPacketAndGenerateFec() 函式:
void UlpfecGenerator::AddPacketAndGenerateFec(const RtpPacketToSend& packet) {
...
const bool complete_frame = packet.Marker();
if (media_packets_.size() < kUlpfecMaxMediaPackets) {
// 構造 fec packet 放入等待佇列中
// Our packet masks can only protect up to |kUlpfecMaxMediaPackets| packets.
auto fec_packet = std::make_unique<ForwardErrorCorrection::Packet>();
fec_packet->data = packet.Buffer();
media_packets_.push_back(std::move(fec_packet));
}
// 這裡每幀計數加1
if (complete_frame) {
++num_protected_frames_;
}
// 動態計算得到 fec 配置引數
auto params = CurrentParams();
// 判斷是否滿足 fec 的條件
// 1. 必須從 complete_frame 開始
// 2. 包數量大於等於 params.max_fec_frames 閾值
// 3. 或者 目前冗餘度低於設定的冗餘度,同時還籌齊了最少 4 個包
if (complete_frame &&
(num_protected_frames_ >= params.max_fec_frames ||
(ExcessOverheadBelowMax() && MinimumMediaPacketsReached()))) {
// 開始 fec 編碼
fec_->EncodeFec(media_packets_, params.fec_rate, kNumImportantPackets,
kUseUnequalProtection, params.fec_mask_type,
&generated_fec_packets_);
}
...
}
需要注意的是:
complete_frame
變數的存在,表明如果一個編碼幀生成了多個 rtp 包,那麼 fec 不會分開他們,同一幀的多個 rtp 包總是被一個 fec 保護
1.2 重新累積
每 k 個資料包生成 fec 後,需要重新累積,參看 UlpfecGenerator::ResetState() 函式:
void UlpfecGenerator::ResetState() {
// 清空原始資料包
media_packets_.clear();
last_media_packet_.reset();
generated_fec_packets_.clear();
num_protected_frames_ = 0;
media_contains_keyframe_ = false;
}
1.3 演算法原理
一個待傳送的 rtp 包,可以看做 [1, n] 即 1 行 n 列形式的資料,其中 n 是位元組數,有 k 個原始資料包,那麼可以得到 D = [k, n] 的資料。
fec 生成可以看做一個生成矩陣 G 乘以 D,現在假設 G = [q, k],那麼 G * D = F,即 F = [q, n]。
以上公式可以描述為:k 個原始資料包,生成了 q 個冗餘包,其中 q <= k。
那麼對於 webrtc fec,G 生成矩陣是如何設計的呢?實際上 G 只由 0 和 1 構成,表示選擇或不選擇,且 G * D 的矩陣加法變為 xor 異或運算,解決資料溢位的問題。
1.4 掩碼錶選擇
上一節說了生成矩陣的選擇,在 webrtc 的實現中,預先配置了大量生成矩陣(或者稱為掩碼),例如 fec_private_tables_random.cc 檔案內:
...
#define kMaskRandom8_4 \
0x45, 0x00, \
0xb4, 0x00, \
0x6a, 0x00, \
0x89, 0x00
...
#define kMaskRandom10_3 \
0xa4, 0x40, \
0xc9, 0x00, \
0x52, 0x80
...
這裡生成矩陣每一行由 16 進位制數表示,kMaskRandom8_4 表示 8 個原始包生成 4 個 fec 包。
實際上 webrtc 對 fec 丟包模型有兩種生成矩陣配置表,即突發丟包和隨機丟包,對應不同掩碼錶。這兩種配置表數值設計的規則是什麼,暫時還不清楚
掩碼錶的選擇,在 ForwardErrorCorrection::EncodeFec() 函式中:
int ForwardErrorCorrection::EncodeFec(const PacketList& media_packets,
uint8_t protection_factor,
int num_important_packets,
bool use_unequal_protection,
FecMaskType fec_mask_type,
std::list<Packet*>* fec_packets) {
// 原始包數量
const size_t num_media_packets = media_packets.size();
// 根據原始包數量和實時配置的冗餘率,得到 fec 包數量
int num_fec_packets = NumFecPackets(num_media_packets, protection_factor);
// 找到掩碼錶
internal::GeneratePacketMasks(num_media_packets, num_fec_packets,
num_important_packets, use_unequal_protection,
&mask_table, packet_masks_);
...
}
1.5 計算生成
現在生成矩陣和原始資料都有了,只需要按矩陣乘法生成 fec 即可,參看 ForwardErrorCorrection::GenerateFecPayloads() 函式:
void ForwardErrorCorrection::GenerateFecPayloads(
const PacketList& media_packets,
size_t num_fec_packets) {
// 遍歷生成每個冗餘包
for (size_t i = 0; i < num_fec_packets; ++i) {
...
// 對於每個冗餘包,都需要遍歷一遍原始資料包,根據掩碼錶確定哪些資料包需要參與本輪 fec 包生成
while (media_packets_it != media_packets.end()) {
Packet* const media_packet = media_packets_it->get();
// Should `media_packet` be protected by `fec_packet`?
// 查掩碼錶,如果為 1,表示當前資料包要參與當前 fec 包生成
if (packet_masks_[pkt_mask_idx] & (1 << (7 - media_pkt_idx))) {
...
XorHeaders(*media_packet, fec_packet);
XorPayloads(*media_packet, media_payload_length, fec_header_size,
fec_packet);
}
media_packets_it++;
...
}
}
}
對於資料包的 header,webrtc 內部會 xor 前 12 位元組(webrtc 不會包含 csrc),參看 ForwardErrorCorrection::XorHeaders() 函式:
void ForwardErrorCorrection::XorHeaders(const Packet& src, Packet* dst) {
uint8_t* dst_data = dst->data.MutableData();
const uint8_t* src_data = src.data.cdata();
// XOR the first 2 bytes of the header: V, P, X, CC, M, PT fields.
dst_data[0] ^= src_data[0];
dst_data[1] ^= src_data[1];
// XOR the length recovery field.
uint8_t src_payload_length_network_order[2];
ByteWriter<uint16_t>::WriteBigEndian(src_payload_length_network_order,
src.data.size() - kRtpHeaderSize);
dst_data[2] ^= src_payload_length_network_order[0];
dst_data[3] ^= src_payload_length_network_order[1];
// XOR the 5th to 8th bytes of the header: the timestamp field.
dst_data[4] ^= src_data[4];
dst_data[5] ^= src_data[5];
dst_data[6] ^= src_data[6];
dst_data[7] ^= src_data[7];
// Skip the 9th to 12th bytes of the header.
}
對於資料包的 payload,webrtc 內部實際上是從 12 位元組的 header 後開始 xor,即 header extension 也會被保護,參看 ForwardErrorCorrection::XorPayloads() 函式:
void ForwardErrorCorrection::XorPayloads(const Packet& src,
size_t payload_length,
size_t dst_offset,
Packet* dst) {
// 如果當前資料包比較長,fec 包需要擴容
if (dst_offset + payload_length > dst->data.size()) {
size_t old_size = dst->data.size();
size_t new_size = dst_offset + payload_length;
dst->data.SetSize(new_size);
memset(dst->data.MutableData() + old_size, 0, new_size - old_size);
}
uint8_t* dst_data = dst->data.MutableData();
const uint8_t* src_data = src.data.cdata();
// 注意 payload_length 即資料包減去 12 位元組後的結果
for (size_t i = 0; i < payload_length; ++i) {
dst_data[dst_offset + i] ^= src_data[kRtpHeaderSize + i];
}
}
1.6 ulp fec 和 flex fec 有什麼區別
實際上 webrtc 生成 fec 的程式碼,即 ForwardErrorCorrection 類,對 ulp 和 flex 都是相同的,即 ulp fec 只存在 level 0 級的全量保護。
唯一區別就是兩者生成的 fec 頭部格式不同:
std::unique_ptr<ForwardErrorCorrection> ForwardErrorCorrection::CreateUlpfec(
uint32_t ssrc) {
std::unique_ptr<FecHeaderReader> fec_header_reader(new UlpfecHeaderReader());
std::unique_ptr<FecHeaderWriter> fec_header_writer(new UlpfecHeaderWriter());
return std::unique_ptr<ForwardErrorCorrection>(new ForwardErrorCorrection(
std::move(fec_header_reader), std::move(fec_header_writer), ssrc, ssrc));
}
std::unique_ptr<ForwardErrorCorrection> ForwardErrorCorrection::CreateFlexfec(
uint32_t ssrc,
uint32_t protected_media_ssrc) {
std::unique_ptr<FecHeaderReader> fec_header_reader(new FlexfecHeaderReader());
std::unique_ptr<FecHeaderWriter> fec_header_writer(new FlexfecHeaderWriter());
return std::unique_ptr<ForwardErrorCorrection>(new ForwardErrorCorrection(
std::move(fec_header_reader), std::move(fec_header_writer), ssrc,
protected_media_ssrc));
}
如上可以看到,兩者只是在 fec_header_reader 和 fec_header_writer 上不一樣。同時 ulp fec 與原始資料流共用同一個 ssrc、序列號,只是 pt 值不同;flex fec 有自己獨立的 ssrc、序列號、pt 值。
2. 打包傳輸
2.1 ulp fec 打包方式
在 UlpfecGenerator::GetFecPackets() 函式中,會先將生成的 fec 包打包為 red 封裝格式,然後打包為 rtp 格式(為什麼不單獨傳輸而是要用 red 封裝?暫時不清楚原因):
std::vector<std::unique_ptr<RtpPacketToSend>> UlpfecGenerator::GetFecPackets() {
...
// 遍歷生成的 fec 包
for (const auto* fec_packet : generated_fec_packets_) {
std::unique_ptr<RtpPacketToSend> red_packet =
std::make_unique<RtpPacketToSend>(*last_media_packet_);
// red 打包,具體參看原始碼
// 實際上這裡 red 打包格式很簡單,只使用了一個 block header,即一個 F + PT 的結構
...
fec_packets.push_back(std::move(red_packet));
}
ResetState();
...
return fec_packets;
}
注意這裡會呼叫 ResetState() 清空狀態,參考前文。
2.2 flex fec 打包方式
在 FlexfecSender::GetFecPackets() 函式中,會將生成的 fec 包打包為 rtp,並且需要注意的是其擁有與原始包獨立的 ssrc、pt、seq 值等欄位:
std::vector<std::unique_ptr<RtpPacketToSend>> FlexfecSender::GetFecPackets() {
...
// 遍歷生成的 fec 包
for (const auto* fec_packet : ulpfec_generator_.generated_fec_packets_) {
std::unique_ptr<RtpPacketToSend> fec_packet_to_send(
new RtpPacketToSend(&rtp_header_extension_map_));
// rtp 打包,具體參看原始碼
...
fec_packets_to_send.push_back(std::move(fec_packet_to_send));
}
if (!fec_packets_to_send.empty()) {
ulpfec_generator_.ResetState();
}
...
return fec_packets_to_send;
}
注意這裡也會呼叫 ResetState() 清空狀態,參考前文。
3. 丟包恢復
在接收到資料包後,會來到 ForwardErrorCorrection::DecodeFec() 函式:
void ForwardErrorCorrection::DecodeFec(const ReceivedPacket& received_packet,
RecoveredPacketList* recovered_packets) {
...
InsertPacket(received_packet, recovered_packets);
AttemptRecovery(recovered_packets);
}
ForwardErrorCorrection::InsertPacket() 會將原始資料包插入到 recovered_packets 佇列,並將 fec 包插入到 received_fec_packets_ 佇列:
void ForwardErrorCorrection::InsertPacket(
const ReceivedPacket& received_packet,
RecoveredPacketList* recovered_packets) {
...
if (received_packet.is_fec) {
InsertFecPacket(*recovered_packets, received_packet);
} else {
InsertMediaPacket(recovered_packets, received_packet);
}
DiscardOldRecoveredPackets(recovered_packets);
}
同時在呼叫 InsertFecPacket() 和 InsertMediaPacket() 函式的時候,會根據 fec 包的 fec header 中的 mask 欄位,將 fec 包與其保護的原始資料包對應起來,以 ForwardErrorCorrection::UpdateCoveringFecPackets() 函式為例:
void ForwardErrorCorrection::UpdateCoveringFecPackets(
const RecoveredPacket& packet) {
for (auto& fec_packet : received_fec_packets_) {
// Is this FEC packet protecting the media packet `packet`?
auto protected_it = absl::c_lower_bound(
fec_packet->protected_packets, &packet, SortablePacket::LessThan());
if (protected_it != fec_packet->protected_packets.end() &&
(*protected_it)->seq_num == packet.seq_num) {
// Found an FEC packet which is protecting `packet`.
(*protected_it)->pkt = packet.pkt;
}
}
}
之後就是呼叫 ForwardErrorCorrection::AttemptRecovery() 函式試圖恢復缺失包了:
void ForwardErrorCorrection::AttemptRecovery(
RecoveredPacketList* recovered_packets) {
// 遍歷收到的 fec 包列表
auto fec_packet_it = received_fec_packets_.begin();
while (fec_packet_it != received_fec_packets_.end()) {
// Search for each FEC packet's protected media packets.
int packets_missing = NumCoveredPacketsMissing(**fec_packet_it);
// We can only recover one packet with an FEC packet.
if (packets_missing == 1) {
// Recovery possible.
// 缺失包恢復
std::unique_ptr<RecoveredPacket> recovered_packet(new RecoveredPacket());
recovered_packet->pkt = nullptr;
if (!RecoverPacket(**fec_packet_it, recovered_packet.get())) {
// Can't recover using this packet, drop it.
fec_packet_it = received_fec_packets_.erase(fec_packet_it);
continue;
}
auto* recovered_packet_ptr = recovered_packet.get();
// 將恢復的缺失包,加入到原始包佇列
recovered_packets->push_back(std::move(recovered_packet));
recovered_packets->sort(SortablePacket::LessThan());
// 更新 fec 和原始包的關聯關係
UpdateCoveringFecPackets(*recovered_packet_ptr);
DiscardOldRecoveredPackets(recovered_packets);
// 當前 fec 包已經不需要了
fec_packet_it = received_fec_packets_.erase(fec_packet_it);
// A packet has been recovered. We need to check the FEC list again, as
// this may allow additional packets to be recovered.
// Restart for first FEC packet.
// 由於新的缺失包的恢復,可能前面的 fec 包也能進行丟包恢復了,所以這裡重新開始遍歷
fec_packet_it = received_fec_packets_.begin();
...
}
}
}
可以看到,只有 packets_missing == 1 即當前 fec 包對應保護的原始包,只缺失一個時,才能恢復,這與 XOR 演算法吻合。
而 NumCoveredPacketsMissing() 函式就是根據 fec 包和保護的原始包的對應關係,判斷缺失包的數量,邏輯比較簡單,這裡不用展示了。