You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
380 lines
14 KiB
380 lines
14 KiB
// Copyright 2019 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "cast/streaming/compound_rtcp_parser.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include "cast/streaming/packet_util.h"
|
|
#include "cast/streaming/rtcp_session.h"
|
|
#include "util/osp_logging.h"
|
|
#include "util/std_util.h"
|
|
|
|
namespace openscreen {
|
|
namespace cast {
|
|
|
|
namespace {
|
|
|
|
// Use the Clock's minimum time value (an impossible value, waaaaay before epoch
|
|
// time) to represent unset time_point values.
|
|
constexpr auto kNullTimePoint = Clock::time_point::min();
|
|
|
|
// Canonicalizes the just-parsed list of packet-specific NACKs so that the
|
|
// CompoundRtcpParser::Client can make several simplifying assumptions when
|
|
// processing the results.
|
|
void CanonicalizePacketNackVector(std::vector<PacketNack>* packets) {
|
|
// First, sort all elements. The sort order is the normal lexicographical
|
|
// ordering, with one exception: The special kAllPacketsLost packet_id value
|
|
// should be treated as coming before all others. This special sort order
|
|
// allows the filtering algorithm below to be simpler, and only require one
|
|
// pass; and the final result will be the normal lexicographically-sorted
|
|
// output the CompoundRtcpParser::Client expects.
|
|
std::sort(packets->begin(), packets->end(),
|
|
[](const PacketNack& a, const PacketNack& b) {
|
|
// Since the comparator is a hot code path, use a simple modular
|
|
// arithmetic trick in lieu of extra branching: When comparing the
|
|
// tuples, map all packet_id values to packet_id + 1, mod 0x10000.
|
|
// This results in the desired sorting behavior since
|
|
// kAllPacketsLost (0xffff) wraps-around to 0x0000, and all other
|
|
// values become N + 1.
|
|
static_assert(static_cast<FramePacketId>(kAllPacketsLost + 1) <
|
|
FramePacketId{0x0000 + 1},
|
|
"comparison requires integer wrap-around");
|
|
return PacketNack{a.frame_id,
|
|
static_cast<FramePacketId>(a.packet_id + 1)} <
|
|
PacketNack{b.frame_id,
|
|
static_cast<FramePacketId>(b.packet_id + 1)};
|
|
});
|
|
|
|
// De-duplicate elements. Two possible cases:
|
|
//
|
|
// 1. Identical elements (same FrameId+FramePacketId).
|
|
// 2. If there are any elements with kAllPacketsLost as the packet ID,
|
|
// prune-out all other elements having the same frame ID, as they are
|
|
// redundant.
|
|
//
|
|
// This is done by walking forwards over the sorted vector and deciding which
|
|
// elements to keep. Those that are kept are stacked-up at the front of the
|
|
// vector. After the "to-keep" pass, the vector is truncated to remove the
|
|
// left-over garbage at the end.
|
|
auto have_it = packets->begin();
|
|
if (have_it != packets->end()) {
|
|
auto kept_it = have_it; // Always keep the first element.
|
|
for (++have_it; have_it != packets->end(); ++have_it) {
|
|
if (have_it->frame_id != kept_it->frame_id ||
|
|
(kept_it->packet_id != kAllPacketsLost &&
|
|
have_it->packet_id != kept_it->packet_id)) { // Keep it.
|
|
++kept_it;
|
|
*kept_it = *have_it;
|
|
}
|
|
}
|
|
packets->erase(++kept_it, packets->end());
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
CompoundRtcpParser::CompoundRtcpParser(RtcpSession* session,
|
|
CompoundRtcpParser::Client* client)
|
|
: session_(session),
|
|
client_(client),
|
|
latest_receiver_timestamp_(kNullTimePoint) {
|
|
OSP_DCHECK(session_);
|
|
OSP_DCHECK(client_);
|
|
}
|
|
|
|
CompoundRtcpParser::~CompoundRtcpParser() = default;
|
|
|
|
bool CompoundRtcpParser::Parse(absl::Span<const uint8_t> buffer,
|
|
FrameId max_feedback_frame_id) {
|
|
// These will contain the results from the various ParseXYZ() methods. None of
|
|
// the results will be dispatched to the Client until the entire parse
|
|
// succeeds.
|
|
Clock::time_point receiver_reference_time = kNullTimePoint;
|
|
absl::optional<RtcpReportBlock> receiver_report;
|
|
FrameId checkpoint_frame_id;
|
|
std::chrono::milliseconds target_playout_delay{};
|
|
std::vector<FrameId> received_frames;
|
|
std::vector<PacketNack> packet_nacks;
|
|
bool picture_loss_indicator = false;
|
|
|
|
// The data contained in |buffer| can be a "compound packet," which means that
|
|
// it can be the concatenation of multiple RTCP packets. The loop here
|
|
// processes each one-by-one.
|
|
while (!buffer.empty()) {
|
|
const auto header = RtcpCommonHeader::Parse(buffer);
|
|
if (!header) {
|
|
return false;
|
|
}
|
|
buffer.remove_prefix(kRtcpCommonHeaderSize);
|
|
if (static_cast<int>(buffer.size()) < header->payload_size) {
|
|
return false;
|
|
}
|
|
const absl::Span<const uint8_t> payload =
|
|
buffer.subspan(0, header->payload_size);
|
|
buffer.remove_prefix(header->payload_size);
|
|
|
|
switch (header->packet_type) {
|
|
case RtcpPacketType::kReceiverReport:
|
|
if (!ParseReceiverReport(payload, header->with.report_count,
|
|
&receiver_report)) {
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case RtcpPacketType::kPayloadSpecific:
|
|
switch (header->with.subtype) {
|
|
case RtcpSubtype::kPictureLossIndicator:
|
|
if (!ParsePictureLossIndicator(payload, &picture_loss_indicator)) {
|
|
return false;
|
|
}
|
|
break;
|
|
case RtcpSubtype::kFeedback:
|
|
if (!ParseFeedback(payload, max_feedback_frame_id,
|
|
&checkpoint_frame_id, &target_playout_delay,
|
|
&received_frames, &packet_nacks)) {
|
|
return false;
|
|
}
|
|
break;
|
|
default:
|
|
// Ignore: Unimplemented or not part of the Cast Streaming spec.
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case RtcpPacketType::kExtendedReports:
|
|
if (!ParseExtendedReports(payload, &receiver_reference_time)) {
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Ignored, unimplemented or not part of the Cast Streaming spec.
|
|
break;
|
|
}
|
|
}
|
|
|
|
// A well-behaved Cast Streaming Receiver will always include a reference time
|
|
// report. This essentially "timestamps" the RTCP packets just parsed.
|
|
// However, the spec does not explicitly require this be included. When it is
|
|
// present, improve the stability of the system by ignoring stale/out-of-order
|
|
// RTCP packets.
|
|
if (receiver_reference_time != kNullTimePoint) {
|
|
// If the packet is out-of-order (e.g., it got delayed/shuffled when going
|
|
// through the network), just ignore it. Since RTCP packets always include
|
|
// all the necessary current state from the peer, dropping them does not
|
|
// mean important signals will be lost. In fact, it can actually be harmful
|
|
// to process compound RTCP packets out-of-order.
|
|
if (latest_receiver_timestamp_ != kNullTimePoint &&
|
|
receiver_reference_time < latest_receiver_timestamp_) {
|
|
return true;
|
|
}
|
|
latest_receiver_timestamp_ = receiver_reference_time;
|
|
client_->OnReceiverReferenceTimeAdvanced(latest_receiver_timestamp_);
|
|
}
|
|
|
|
// At this point, the packet is known to be well-formed. Dispatch events of
|
|
// interest to the Client.
|
|
if (receiver_report) {
|
|
client_->OnReceiverReport(*receiver_report);
|
|
}
|
|
if (!checkpoint_frame_id.is_null()) {
|
|
client_->OnReceiverCheckpoint(checkpoint_frame_id, target_playout_delay);
|
|
}
|
|
if (!received_frames.empty()) {
|
|
OSP_DCHECK(AreElementsSortedAndUnique(received_frames));
|
|
client_->OnReceiverHasFrames(std::move(received_frames));
|
|
}
|
|
CanonicalizePacketNackVector(&packet_nacks);
|
|
if (!packet_nacks.empty()) {
|
|
client_->OnReceiverIsMissingPackets(std::move(packet_nacks));
|
|
}
|
|
if (picture_loss_indicator) {
|
|
client_->OnReceiverIndicatesPictureLoss();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CompoundRtcpParser::ParseReceiverReport(
|
|
absl::Span<const uint8_t> in,
|
|
int num_report_blocks,
|
|
absl::optional<RtcpReportBlock>* receiver_report) {
|
|
if (in.size() < kRtcpReceiverReportSize) {
|
|
return false;
|
|
}
|
|
if (ConsumeField<uint32_t>(&in) == session_->receiver_ssrc()) {
|
|
*receiver_report = RtcpReportBlock::ParseOne(in, num_report_blocks,
|
|
session_->sender_ssrc());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CompoundRtcpParser::ParseFeedback(
|
|
absl::Span<const uint8_t> in,
|
|
FrameId max_feedback_frame_id,
|
|
FrameId* checkpoint_frame_id,
|
|
std::chrono::milliseconds* target_playout_delay,
|
|
std::vector<FrameId>* received_frames,
|
|
std::vector<PacketNack>* packet_nacks) {
|
|
OSP_DCHECK(!max_feedback_frame_id.is_null());
|
|
|
|
if (static_cast<int>(in.size()) < kRtcpFeedbackHeaderSize) {
|
|
return false;
|
|
}
|
|
if (ConsumeField<uint32_t>(&in) != session_->receiver_ssrc() ||
|
|
ConsumeField<uint32_t>(&in) != session_->sender_ssrc()) {
|
|
return true; // Ignore report from mismatched SSRC(s).
|
|
}
|
|
if (ConsumeField<uint32_t>(&in) != kRtcpCastIdentifierWord) {
|
|
return false;
|
|
}
|
|
|
|
const FrameId feedback_frame_id =
|
|
max_feedback_frame_id.ExpandLessThanOrEqual(ConsumeField<uint8_t>(&in));
|
|
const int loss_field_count = ConsumeField<uint8_t>(&in);
|
|
const auto playout_delay =
|
|
std::chrono::milliseconds(ConsumeField<uint16_t>(&in));
|
|
// Don't process feedback that would move the checkpoint backwards. The Client
|
|
// makes assumptions about what frame data and other tracking state can be
|
|
// discarded based on a monotonically non-decreasing checkpoint FrameId.
|
|
if (!checkpoint_frame_id->is_null() &&
|
|
*checkpoint_frame_id > feedback_frame_id) {
|
|
return true;
|
|
}
|
|
*checkpoint_frame_id = feedback_frame_id;
|
|
*target_playout_delay = playout_delay;
|
|
received_frames->clear();
|
|
packet_nacks->clear();
|
|
if (static_cast<int>(in.size()) <
|
|
(kRtcpFeedbackLossFieldSize * loss_field_count)) {
|
|
return false;
|
|
}
|
|
|
|
// Parse the NACKs.
|
|
for (int i = 0; i < loss_field_count; ++i) {
|
|
const FrameId frame_id =
|
|
feedback_frame_id.ExpandGreaterThan(ConsumeField<uint8_t>(&in));
|
|
FramePacketId packet_id = ConsumeField<uint16_t>(&in);
|
|
uint8_t bits = ConsumeField<uint8_t>(&in);
|
|
packet_nacks->push_back(PacketNack{frame_id, packet_id});
|
|
|
|
if (packet_id != kAllPacketsLost) {
|
|
// Translate each set bit in the bit vector into another missing
|
|
// FramePacketId.
|
|
while (bits) {
|
|
++packet_id;
|
|
if (bits & 1) {
|
|
packet_nacks->push_back(PacketNack{frame_id, packet_id});
|
|
}
|
|
bits >>= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse the optional CST2 feedback (frame-level ACKs).
|
|
if (static_cast<int>(in.size()) < kRtcpFeedbackAckHeaderSize ||
|
|
ConsumeField<uint32_t>(&in) != kRtcpCst2IdentifierWord) {
|
|
// Optional CST2 extended feedback is not present. For backwards-
|
|
// compatibility reasons, do not consider any extra "garbage" in the packet
|
|
// that doesn't match 'CST2' as corrupted input.
|
|
return true;
|
|
}
|
|
// Skip over the "Feedback Count" field. It's currently unused, though it
|
|
// might be useful for event tracing later...
|
|
in.remove_prefix(sizeof(uint8_t));
|
|
const int ack_bitvector_octet_count = ConsumeField<uint8_t>(&in);
|
|
if (static_cast<int>(in.size()) < ack_bitvector_octet_count) {
|
|
return false;
|
|
}
|
|
// Translate each set bit in the bit vector into a FrameId. See the
|
|
// explanation of this wire format in rtp_defines.h for where the "plus two"
|
|
// comes from.
|
|
FrameId starting_frame_id = feedback_frame_id + 2;
|
|
for (int i = 0; i < ack_bitvector_octet_count; ++i) {
|
|
uint8_t bits = ConsumeField<uint8_t>(&in);
|
|
FrameId frame_id = starting_frame_id;
|
|
while (bits) {
|
|
if (bits & 1) {
|
|
received_frames->push_back(frame_id);
|
|
}
|
|
++frame_id;
|
|
bits >>= 1;
|
|
}
|
|
constexpr int kBitsPerOctet = 8;
|
|
starting_frame_id += kBitsPerOctet;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CompoundRtcpParser::ParseExtendedReports(
|
|
absl::Span<const uint8_t> in,
|
|
Clock::time_point* receiver_reference_time) {
|
|
if (static_cast<int>(in.size()) < kRtcpExtendedReportHeaderSize) {
|
|
return false;
|
|
}
|
|
if (ConsumeField<uint32_t>(&in) != session_->receiver_ssrc()) {
|
|
return true; // Ignore report from unknown receiver.
|
|
}
|
|
|
|
while (!in.empty()) {
|
|
// All extended report types have the same 4-byte subheader.
|
|
if (static_cast<int>(in.size()) < kRtcpExtendedReportBlockHeaderSize) {
|
|
return false;
|
|
}
|
|
const uint8_t block_type = ConsumeField<uint8_t>(&in);
|
|
in.remove_prefix(sizeof(uint8_t)); // Skip the "reserved" byte.
|
|
const int block_data_size =
|
|
static_cast<int>(ConsumeField<uint16_t>(&in)) * 4;
|
|
if (static_cast<int>(in.size()) < block_data_size) {
|
|
return false;
|
|
}
|
|
if (block_type == kRtcpReceiverReferenceTimeReportBlockType) {
|
|
if (block_data_size != sizeof(uint64_t)) {
|
|
return false; // Length field must always be 2 words.
|
|
}
|
|
*receiver_reference_time = session_->ntp_converter().ToLocalTime(
|
|
ReadBigEndian<uint64_t>(in.data()));
|
|
} else {
|
|
// Ignore any other type of extended report.
|
|
}
|
|
in.remove_prefix(block_data_size);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CompoundRtcpParser::ParsePictureLossIndicator(
|
|
absl::Span<const uint8_t> in,
|
|
bool* picture_loss_indicator) {
|
|
if (static_cast<int>(in.size()) < kRtcpPictureLossIndicatorHeaderSize) {
|
|
return false;
|
|
}
|
|
// Only set the flag if the PLI is from the Receiver and to this Sender.
|
|
if (ConsumeField<uint32_t>(&in) == session_->receiver_ssrc() &&
|
|
ConsumeField<uint32_t>(&in) == session_->sender_ssrc()) {
|
|
*picture_loss_indicator = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
CompoundRtcpParser::Client::Client() = default;
|
|
CompoundRtcpParser::Client::~Client() = default;
|
|
void CompoundRtcpParser::Client::OnReceiverReferenceTimeAdvanced(
|
|
Clock::time_point reference_time) {}
|
|
void CompoundRtcpParser::Client::OnReceiverReport(
|
|
const RtcpReportBlock& receiver_report) {}
|
|
void CompoundRtcpParser::Client::OnReceiverIndicatesPictureLoss() {}
|
|
void CompoundRtcpParser::Client::OnReceiverCheckpoint(
|
|
FrameId frame_id,
|
|
std::chrono::milliseconds playout_delay) {}
|
|
void CompoundRtcpParser::Client::OnReceiverHasFrames(
|
|
std::vector<FrameId> acks) {}
|
|
void CompoundRtcpParser::Client::OnReceiverIsMissingPackets(
|
|
std::vector<PacketNack> nacks) {}
|
|
|
|
} // namespace cast
|
|
} // namespace openscreen
|