| /* |
| * 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 |