Upstream commit: https://webrtc.googlesource.com/src/+/9017801a374d091654f75f360182b00d5565e24f Add SFrame packet buffer for RTP-level frame assembly This CL adds the foundational components for SFrame receive-side support in WebRTC, as specified in draft-ietf-avtcore-rtp-sframe. These components sit upstream of the existing PacketBuffer and handle SFrame-specific parsing and frame boundary validation before packets enter the standard video receive pipeline. Three new components, with BUILD.gn targets and unit tests: SFrameDescriptor -- header-only struct representing the 1-byte SFrame payload descriptor (S/E/T bits: start-of-frame, end-of-frame, per-packet vs per-frame mode). SframeRtpPacketReceived -- lightweight wrapper pairing an RtpPacketReceived with its parsed SFrameDescriptor, so downstream code can inspect S/E/T bits without re-parsing. SFramePacketBuffer -- circular buffer that collects SFrame RTP packets and validates complete S->E runs (contiguous start-to-end sequences) before releasing them for decryption and depacketization. Follows the same circular-buffer pattern as the existing PacketBuffer: starts at 96 slots, doubles up to 2048, validates frame consistency (T-bit, payload type, timestamp), and supports ClearTo for window management. Why this is needed SFrame (draft-ietf-avtcore-rtp-sframe) adds a 1-byte payload descriptor to each RTP packet indicating frame boundaries and encryption mode. Before SFrame packets can enter the existing codec depacketizer pipeline, we need to: 1. Parse and strip the SFrame descriptor byte 2. Collect packets into complete frames (S->E runs) at the RTP level 3. Branch on the T-bit to determine whether decryption is per-packet (T=1) or per-frame (T=0) The existing PacketBuffer operates on codec-depacketized payloads and cannot perform this pre-processing. A separate buffer layer is required. More on architecture plan: https://github.com/webex/webrtc-sframe/tree/sframe-receive-pipeline Bug: webrtc:479862368 Change-Id: I40c076739a7e237eae344a6a3282948c53d5420b Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/466920 Reviewed-by: Philip Eliasson <philipel@webrtc.org> Reviewed-by: Harald Alvestrand <hta@webrtc.org> Commit-Queue: Harald Alvestrand <hta@webrtc.org> Cr-Commit-Position: refs/heads/main@{#47856}
308 lines
9.2 KiB
C++
308 lines
9.2 KiB
C++
/*
|
|
* Copyright (c) 2026 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree. An additional intellectual property rights grant can be found
|
|
* in the file PATENTS. All contributing project authors may
|
|
* be found in the AUTHORS file in the root of the source tree.
|
|
*/
|
|
|
|
#include "modules/video_coding/sframe_packet_buffer.h"
|
|
|
|
#include <algorithm>
|
|
#include <bit>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <utility>
|
|
#include <variant>
|
|
|
|
#include "absl/strings/string_view.h"
|
|
#include "modules/rtp_rtcp/source/sframe_descriptor.h"
|
|
#include "modules/rtp_rtcp/source/sframe_rtp_packet_received.h"
|
|
#include "rtc_base/checks.h"
|
|
#include "rtc_base/logging.h"
|
|
|
|
namespace webrtc {
|
|
|
|
SFramePacketBuffer::SFramePacketBuffer(size_t buffer_size)
|
|
: buffer_(buffer_size) {
|
|
// Buffer size must always be a power of 2.
|
|
RTC_DCHECK(std::has_single_bit(buffer_size));
|
|
}
|
|
|
|
SFramePacketBuffer::~SFramePacketBuffer() = default;
|
|
|
|
std::variant<SFramePacketBuffer::AssembledFrame,
|
|
SFramePacketBuffer::InsertResult>
|
|
SFramePacketBuffer::InsertPacket(
|
|
std::unique_ptr<SframeRtpPacketReceived> packet) {
|
|
int64_t seq_num = seq_num_unwrapper_.Unwrap(packet->SequenceNumber());
|
|
bool buffer_cleared = false;
|
|
|
|
// Adjust the receive window and reject packets that are too old.
|
|
if (!UpdateWindowStart(seq_num)) {
|
|
return InsertResult::kNoFrame; // Too old, drop.
|
|
}
|
|
|
|
// Find or make room for this packet in the circular buffer.
|
|
auto [outcome, slot] = ResolveSlot(seq_num);
|
|
switch (outcome) {
|
|
case SlotOutcome::kDuplicate:
|
|
return InsertResult::kNoFrame;
|
|
case SlotOutcome::kFull:
|
|
// Buffer full — clear and reclaim the slot.
|
|
RTC_LOG(LS_WARNING) << "SFramePacketBuffer full, clearing.";
|
|
Clear();
|
|
buffer_cleared = true;
|
|
first_seq_num_ = seq_num;
|
|
slot = ToIdx(seq_num);
|
|
break;
|
|
case SlotOutcome::kOk:
|
|
break;
|
|
}
|
|
|
|
buffer_[slot] = std::move(packet);
|
|
|
|
if (buffer_cleared) {
|
|
return InsertResult::kBufferCleared;
|
|
}
|
|
|
|
// Check whether this insertion completes a frame.
|
|
if (std::optional<AssembledFrame> frame = FindFrame(seq_num)) {
|
|
return std::move(*frame);
|
|
}
|
|
|
|
return InsertResult::kNoFrame;
|
|
}
|
|
|
|
std::pair<SFramePacketBuffer::SlotOutcome, size_t>
|
|
SFramePacketBuffer::ResolveSlot(int64_t seq_num) {
|
|
size_t idx = ToIdx(seq_num);
|
|
|
|
if (!buffer_[idx]) {
|
|
return {SlotOutcome::kOk, idx}; // Empty slot.
|
|
}
|
|
|
|
if (buffer_[idx]->SequenceNumber() == static_cast<uint16_t>(seq_num)) {
|
|
return {SlotOutcome::kDuplicate, 0}; // Duplicate.
|
|
}
|
|
|
|
// Slot collision with a different seq num — buffer is full.
|
|
return {SlotOutcome::kFull, 0};
|
|
}
|
|
|
|
std::optional<SFramePacketBuffer::AssembledFrame> SFramePacketBuffer::FindFrame(
|
|
int64_t seq_num) {
|
|
// Walk backward to find the S=1 (start-of-frame) packet.
|
|
std::optional<int64_t> start = FindFrameStart(seq_num);
|
|
if (!start) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Walk forward to find the E=1 (end-of-frame) packet.
|
|
std::optional<int64_t> end = FindFrameEnd(seq_num, *start);
|
|
if (!end) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Validate consistency and extract the complete frame.
|
|
return AssembleFrame(*start, *end);
|
|
}
|
|
|
|
std::optional<int64_t> SFramePacketBuffer::FindFrameStart(int64_t seq_num) {
|
|
// Walk backward one packet at a time from the insertion point.
|
|
int64_t start = seq_num;
|
|
for (size_t steps = 0; steps < buffer_.size(); ++steps) {
|
|
size_t idx = ToIdx(start);
|
|
|
|
// Slot is empty — packet hasn't arrived yet.
|
|
if (!buffer_[idx]) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Slot holds a packet from a different sequence number (modulo aliasing).
|
|
if (buffer_[idx]->SequenceNumber() != static_cast<uint16_t>(start)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Reached the first packet of the frame (S-bit set).
|
|
if (buffer_[idx]->descriptor().start) {
|
|
return start;
|
|
}
|
|
|
|
// Reached the oldest packet in our window without finding S-bit.
|
|
if (start == *first_seq_num_) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
--start; // Continue walking backward.
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<int64_t> SFramePacketBuffer::FindFrameEnd(int64_t seq_num,
|
|
int64_t start) {
|
|
// Walk forward one packet at a time from the insertion point.
|
|
int64_t end = seq_num;
|
|
while (true) {
|
|
size_t idx = ToIdx(end);
|
|
|
|
// Slot is empty — packet hasn't arrived yet.
|
|
if (!buffer_[idx]) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Slot holds a packet from a different sequence number (modulo aliasing).
|
|
if (buffer_[idx]->SequenceNumber() != static_cast<uint16_t>(end)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Reached the last packet of the frame (E-bit set).
|
|
if (buffer_[idx]->descriptor().end) {
|
|
return end;
|
|
}
|
|
|
|
++end; // Continue walking forward.
|
|
|
|
// Prevent scanning beyond the buffer capacity.
|
|
if (end - start >= static_cast<int64_t>(buffer_.size())) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::optional<SFramePacketBuffer::AssembledFrame>
|
|
SFramePacketBuffer::AssembleFrame(int64_t start, int64_t end) {
|
|
// Number of packets in the [start, end] range.
|
|
const int64_t frame_size = end - start + 1;
|
|
|
|
// Use the first packet as reference for consistency checks.
|
|
const SframeRtpPacketReceived& first_pkt = *buffer_[ToIdx(start)];
|
|
const SframeEncryptionLevel encryption_level =
|
|
first_pkt.descriptor().encryption_level;
|
|
const uint8_t pt = first_pkt.PayloadType();
|
|
const uint32_t ts = first_pkt.Timestamp();
|
|
|
|
// Verify that all packets in the frame share the same T-bit, PT, and
|
|
// timestamp. A mismatch means corrupted or spurious packets slipped in.
|
|
int64_t s = start;
|
|
for (int64_t i = 0; i < frame_size; ++i, ++s) {
|
|
const SframeRtpPacketReceived& pkt = *buffer_[ToIdx(s)];
|
|
if (pkt.descriptor().encryption_level != encryption_level) {
|
|
DropFrame("T-bit mismatch", pkt.SequenceNumber(), start, end);
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (pkt.PayloadType() != pt) {
|
|
DropFrame("payload type mismatch", pkt.SequenceNumber(), start, end);
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (pkt.Timestamp() != ts) {
|
|
DropFrame("timestamp mismatch", pkt.SequenceNumber(), start, end);
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
// Validation passed — move packets out of the buffer into the result.
|
|
AssembledFrame result;
|
|
result.encryption_level = encryption_level;
|
|
result.packets.reserve(frame_size);
|
|
|
|
s = start;
|
|
for (int64_t i = 0; i < frame_size; ++i, ++s) {
|
|
size_t idx = ToIdx(s);
|
|
result.packets.push_back(buffer_[idx]->TakePacket());
|
|
buffer_[idx].reset();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void SFramePacketBuffer::ClearTo(uint16_t seq_num) {
|
|
// Nothing to clear if the buffer hasn't been used yet.
|
|
if (!first_seq_num_) {
|
|
return;
|
|
}
|
|
|
|
int64_t unwrapped = seq_num_unwrapper_.PeekUnwrap(seq_num);
|
|
|
|
// Ignore if the window already starts past the requested point.
|
|
if (*first_seq_num_ > unwrapped) {
|
|
return;
|
|
}
|
|
|
|
// ClearTo is inclusive, so advance one past the target.
|
|
int64_t target = unwrapped + 1;
|
|
|
|
// Walk from the current window start to the target, clearing slots whose
|
|
// packets fall within the cleared range. Only reset slots that actually
|
|
// belong to the old range (a slot may hold a newer packet due to modulo
|
|
// aliasing).
|
|
int64_t diff = target - *first_seq_num_;
|
|
int64_t iterations = std::min(diff, static_cast<int64_t>(buffer_.size()));
|
|
int64_t cur = *first_seq_num_;
|
|
for (int64_t i = 0; i < iterations; ++i) {
|
|
auto& slot = buffer_[ToIdx(cur)];
|
|
if (slot && slot->SequenceNumber() == static_cast<uint16_t>(cur)) {
|
|
slot.reset();
|
|
}
|
|
++cur;
|
|
}
|
|
|
|
// Advance the window start and lock it so late packets can't move it back.
|
|
first_seq_num_ = target;
|
|
is_cleared_to_first_seq_num_ = true;
|
|
}
|
|
|
|
void SFramePacketBuffer::Clear() {
|
|
for (auto& slot : buffer_) {
|
|
slot.reset();
|
|
}
|
|
first_seq_num_.reset();
|
|
is_cleared_to_first_seq_num_ = false;
|
|
}
|
|
|
|
void SFramePacketBuffer::DropFrame(absl::string_view reason,
|
|
uint16_t bad_seq,
|
|
int64_t start,
|
|
int64_t end) {
|
|
RTC_LOG(LS_WARNING) << "SFramePacketBuffer: " << reason << ", seq=" << bad_seq
|
|
<< ". Dropping frame [" << start << ".." << end << "].";
|
|
const int64_t frame_size = end - start + 1;
|
|
int64_t s = start;
|
|
for (int64_t i = 0; i < frame_size; ++i, ++s) {
|
|
buffer_[ToIdx(s)].reset();
|
|
}
|
|
}
|
|
|
|
bool SFramePacketBuffer::UpdateWindowStart(int64_t seq_num) {
|
|
// First packet ever — initialize the window.
|
|
if (!first_seq_num_) {
|
|
first_seq_num_ = seq_num;
|
|
} else if (*first_seq_num_ > seq_num) {
|
|
// Packet arrived before our current window start (reordered).
|
|
|
|
// ClearTo was called — the window start was explicitly set and we must
|
|
// not move it backward.
|
|
if (is_cleared_to_first_seq_num_) {
|
|
return false;
|
|
}
|
|
|
|
// Packet is so old it wouldn't fit in the buffer.
|
|
if (*first_seq_num_ - seq_num >= static_cast<int64_t>(buffer_.size())) {
|
|
return false;
|
|
}
|
|
|
|
// Extend the window backward to include this reordered packet.
|
|
first_seq_num_ = seq_num;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace webrtc
|