blob: bdf39d20bdd7245a1832c2f6315dda116e106b6e [file] [edit]
/*
* 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