/* * 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 #include #include #include #include #include #include #include #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::InsertPacket( std::unique_ptr 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 frame = FindFrame(seq_num)) { return std::move(*frame); } return InsertResult::kNoFrame; } std::pair 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(seq_num)) { return {SlotOutcome::kDuplicate, 0}; // Duplicate. } // Slot collision with a different seq num — buffer is full. return {SlotOutcome::kFull, 0}; } std::optional SFramePacketBuffer::FindFrame( int64_t seq_num) { // Walk backward to find the S=1 (start-of-frame) packet. std::optional start = FindFrameStart(seq_num); if (!start) { return std::nullopt; } // Walk forward to find the E=1 (end-of-frame) packet. std::optional end = FindFrameEnd(seq_num, *start); if (!end) { return std::nullopt; } // Validate consistency and extract the complete frame. return AssembleFrame(*start, *end); } std::optional 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(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 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(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(buffer_.size())) { return std::nullopt; } } } std::optional 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(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(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(buffer_.size())) { return false; } // Extend the window backward to include this reordered packet. first_seq_num_ = seq_num; } return true; } } // namespace webrtc