diff options
author | Yuri Wiitala <miu@chromium.org> | 2019-11-22 15:04:27 -0800 |
---|---|---|
committer | Commit Bot <commit-bot@chromium.org> | 2019-11-22 23:17:56 +0000 |
commit | 7d2583a9952b389760fbec7c9eed515bb2270fb4 (patch) | |
tree | 6929ab25ae7972e283eb866d04be9c0ef7583815 /cast/streaming | |
parent | ef98b330b71a64ef3a427b93fcfa92a76c06a597 (diff) | |
download | openscreen-7d2583a9952b389760fbec7c9eed515bb2270fb4.tar.gz |
Move streaming/cast/* to cast/streaming/* and cast/standalone_receiver/*
Moves the files from streaming/cast to cast/streaming, fixes #includes
and header guards, adopts the cast::streaming namespace, and patches
BUILD.gn and DEPS files.
Change-Id: I77d467a98823f3c55ed38f9a298967e3a42d0549
Reviewed-on: https://chromium-review.googlesource.com/c/openscreen/+/1927135
Commit-Queue: Yuri Wiitala <miu@chromium.org>
Reviewed-by: Jordan Bayles <jophba@chromium.org>
Diffstat (limited to 'cast/streaming')
105 files changed, 9677 insertions, 28 deletions
diff --git a/cast/streaming/BUILD.gn b/cast/streaming/BUILD.gn index acafd228..d45ee925 100644 --- a/cast/streaming/BUILD.gn +++ b/cast/streaming/BUILD.gn @@ -2,25 +2,196 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -# TODO(jophba): merge with streaming source set after streaming/cast merge. -source_set("streaming_configs") { +import("//build_overrides/build.gni") + +source_set("common") { sources = [ + "clock_drift_smoother.cc", + "clock_drift_smoother.h", + "constants.h", + "encoded_frame.cc", + "encoded_frame.h", + "environment.cc", + "environment.h", + "expanded_value_base.h", + "frame_crypto.cc", + "frame_crypto.h", + "frame_id.cc", + "frame_id.h", + "ntp_time.cc", + "ntp_time.h", + "packet_util.cc", + "packet_util.h", + "rtcp_common.cc", + "rtcp_common.h", + "rtcp_session.cc", + "rtcp_session.h", + "rtp_defines.cc", + "rtp_defines.h", + "rtp_time.cc", + "rtp_time.h", "session_config.cc", "session_config.h", + "ssrc.cc", + "ssrc.h", ] public_configs = [ "../../build:openscreen_include_dirs" ] + + public_deps = [ + "../../third_party/abseil", + "../../third_party/boringssl", + ] + + deps = [ + "../../platform", + "../../util", + ] } -source_set("streaming") { +source_set("receiver") { sources = [ + "compound_rtcp_builder.cc", + "compound_rtcp_builder.h", + "frame_collector.cc", + "frame_collector.h", + "packet_receive_stats_tracker.cc", + "packet_receive_stats_tracker.h", + "receiver.cc", + "receiver.h", + "receiver_packet_router.cc", + "receiver_packet_router.h", "receiver_session.cc", "receiver_session.h", + "rtp_packet_parser.cc", + "rtp_packet_parser.h", + "sender_report_parser.cc", + "sender_report_parser.h", ] public_deps = [ - ":streaming_configs", - "../../streaming/cast:common", - "../../streaming/cast:receiver", + ":common", + ] +} + +source_set("sender") { + sources = [ + "compound_rtcp_parser.cc", + "compound_rtcp_parser.h", + "rtp_packetizer.cc", + "rtp_packetizer.h", + "sender_report_builder.cc", + "sender_report_builder.h", + ] + + public_deps = [ + ":common", + ] +} + +source_set("unittests") { + testonly = true + + sources = [ + "compound_rtcp_builder_unittest.cc", + "compound_rtcp_parser_unittest.cc", + "expanded_value_base_unittest.cc", + "frame_collector_unittest.cc", + "frame_crypto_unittest.cc", + "mock_compound_rtcp_parser_client.h", + "ntp_time_unittest.cc", + "packet_receive_stats_tracker_unittest.cc", + "packet_util_unittest.cc", + "receiver_unittest.cc", + "rtcp_common_unittest.cc", + "rtp_packet_parser_unittest.cc", + "rtp_packetizer_unittest.cc", + "rtp_time_unittest.cc", + "sender_report_unittest.cc", + "ssrc_unittest.cc", + ] + + deps = [ + ":receiver", + ":sender", + "../../third_party/googletest:gmock", + "../../third_party/googletest:gtest", ] } + +if (build_with_chromium) { + import("//testing/libfuzzer/fuzzer_test.gni") + + fuzzer_test("compound_rtcp_parser_fuzzer") { + sources = [ + "compound_rtcp_parser_fuzzer.cc", + ] + + deps = [ + ":sender", + "../../third_party/abseil", + ] + + seed_corpus = "compound_rtcp_parser_fuzzer_seeds" + + # Note: 1500 is approx. kMaxRtpPacketSize in rtp_defines.h. + libfuzzer_options = [ "max_len=1500" ] + } + + fuzzer_test("rtp_packet_parser_fuzzer") { + sources = [ + "rtp_packet_parser_fuzzer.cc", + ] + + deps = [ + ":receiver", + "../../third_party/abseil", + ] + + seed_corpus = "rtp_packet_parser_fuzzer_seeds" + + # Note: 1500 is approx. kMaxRtpPacketSize in rtp_defines.h. + libfuzzer_options = [ "max_len=1500" ] + } + + fuzzer_test("sender_report_parser_fuzzer") { + sources = [ + "sender_report_parser_fuzzer.cc", + ] + + deps = [ + ":receiver", + "../../third_party/abseil", + ] + + seed_corpus = "sender_report_parser_fuzzer_seeds" + + # Note: 1500 is approx. kMaxRtpPacketSize in rtp_defines.h. + libfuzzer_options = [ "max_len=1500" ] + } +} else { + # Note: The following is commented out because, as of this writing, the LLVM + # toolchain we pull does not include libclang_rt.fuzzer-x86_64.a, the + # libFuzzer library *with* a main() to drive everything. Thus, the only way to + # get things working is to specify an exact path to the fuzzer_no_main variant + # of the library that *is* avalable, and then provide our own main(). In + # summary, what you see below demonstrates how to get it working specifically + # for Clang 9.0.0 on Linux x86_64. One need only modify the "libs = [...]" for + # a different Clang, OS, or architecture. + # if (is_clang) { + # executable("rtp_packet_parser_fuzzer") { + # testonly = true + # defines = [ "NEEDS_MAIN_TO_CALL_FUZZER_DRIVER" ] + # sources = [ + # "rtp_packet_parser_fuzzer.cc", + # ] + # cflags_cc = [ "-fsanitize=address,fuzzer-no-link,undefined" ] + # ldflags = [ "-fsanitize=address,undefined" ] + # libs = [ "$clang_base_path/lib/clang/9.0.0/lib/linux/libclang_rt.fuzzer_no_main-x86_64.a" ] + # deps = [ + # ":receiver", + # "../../third_party/abseil", + # ] + # } + # } +} diff --git a/cast/streaming/DEPS b/cast/streaming/DEPS index 8f5d55c3..03a77eea 100644 --- a/cast/streaming/DEPS +++ b/cast/streaming/DEPS @@ -1,13 +1,10 @@ -# -*- Mode: Python; -*- +# 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. -# In the future, the streaming code will likely split into sender and receiver -# modules, at which point this can become finer-grained. include_rules = [ '+cast/common', '+cast/receiver', '+cast/sender', - '+cast/streaming', - - # TODO(jophba): remove after streaming/cast code is migrated. - '+streaming/cast' + '+openssl' ] diff --git a/cast/streaming/clock_drift_smoother.cc b/cast/streaming/clock_drift_smoother.cc new file mode 100644 index 00000000..bb829578 --- /dev/null +++ b/cast/streaming/clock_drift_smoother.cc @@ -0,0 +1,72 @@ +// Copyright 2014 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/clock_drift_smoother.h" + +#include <cmath> + +#include "util/logging.h" + +namespace cast { +namespace streaming { + +namespace { +constexpr ClockDriftSmoother::Clock::time_point kNullTime = + ClockDriftSmoother::Clock::time_point::min(); +} + +ClockDriftSmoother::ClockDriftSmoother(Clock::duration time_constant) + : time_constant_(time_constant), + last_update_time_(kNullTime), + estimated_tick_offset_(0.0) { + OSP_DCHECK(time_constant_ > decltype(time_constant_)::zero()); +} + +ClockDriftSmoother::~ClockDriftSmoother() = default; + +ClockDriftSmoother::Clock::duration ClockDriftSmoother::Current() const { + OSP_DCHECK(last_update_time_ != kNullTime); + const double rounded_estimate = std::round(estimated_tick_offset_); + if (rounded_estimate < Clock::duration::min().count()) { + return Clock::duration::min(); + } else if (rounded_estimate > Clock::duration::max().count()) { + return Clock::duration::max(); + } + return Clock::duration(static_cast<Clock::duration::rep>(rounded_estimate)); +} + +void ClockDriftSmoother::Reset(Clock::time_point now, + Clock::duration measured_offset) { + OSP_DCHECK(now != kNullTime); + last_update_time_ = now; + estimated_tick_offset_ = static_cast<double>(measured_offset.count()); +} + +void ClockDriftSmoother::Update(Clock::time_point now, + Clock::duration measured_offset) { + OSP_DCHECK(now != kNullTime); + if (last_update_time_ == kNullTime) { + Reset(now, measured_offset); + } else if (now < last_update_time_) { + // |now| is not monotonically non-decreasing. + OSP_NOTREACHED(); + } else { + const double elapsed_ticks = + static_cast<double>((now - last_update_time_).count()); + last_update_time_ = now; + // Compute a weighted-average between the last estimate and + // |measured_offset|. The more time that has elasped since the last call to + // Update(), the more-heavily |measured_offset| will be weighed. + const double weight = + elapsed_ticks / (elapsed_ticks + time_constant_.count()); + estimated_tick_offset_ = weight * measured_offset.count() + + (1.0 - weight) * estimated_tick_offset_; + } +} + +// static +constexpr std::chrono::seconds ClockDriftSmoother::kDefaultTimeConstant; + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/clock_drift_smoother.h b/cast/streaming/clock_drift_smoother.h new file mode 100644 index 00000000..d48d166c --- /dev/null +++ b/cast/streaming/clock_drift_smoother.h @@ -0,0 +1,60 @@ +// Copyright 2014 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. + +#ifndef CAST_STREAMING_CLOCK_DRIFT_SMOOTHER_H_ +#define CAST_STREAMING_CLOCK_DRIFT_SMOOTHER_H_ + +#include <chrono> + +#include "platform/api/time.h" + +namespace cast { +namespace streaming { + +// Tracks the jitter and drift between clocks, providing a smoothed offset. +// Internally, a Simple IIR filter is used to maintain a running average that +// moves at a rate based on the passage of time. +class ClockDriftSmoother { + public: + using Clock = openscreen::platform::Clock; + + // |time_constant| is the amount of time an impulse signal takes to decay by + // ~62.6%. Interpretation: If the value passed to several Update() calls is + // held constant for T seconds, then the running average will have moved + // towards the value by ~62.6% from where it started. + explicit ClockDriftSmoother(Clock::duration time_constant); + ~ClockDriftSmoother(); + + // Returns the current offset. + Clock::duration Current() const; + + // Discard all history and reset to exactly |offset|, measured |now|. + void Reset(Clock::time_point now, Clock::duration offset); + + // Update the current offset, which was measured |now|. The weighting that + // |measured_offset| will have on the running average is influenced by how + // much time has passed since the last call to this method (or Reset()). + // |now| should be monotonically non-decreasing over successive calls of this + // method. + void Update(Clock::time_point now, Clock::duration measured_offset); + + // A time constant suitable for most use cases, where the clocks are expected + // to drift very little with respect to each other, and the jitter caused by + // clock imprecision is effectively canceled out. + static constexpr std::chrono::seconds kDefaultTimeConstant{30}; + + private: + const std::chrono::duration<double, Clock::duration::period> time_constant_; + + // The time at which |estimated_tick_offset_| was last updated. + Clock::time_point last_update_time_; + + // The current estimated offset, as number of Clock::duration ticks. + double estimated_tick_offset_; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_CLOCK_DRIFT_SMOOTHER_H_ diff --git a/cast/streaming/compound_rtcp_builder.cc b/cast/streaming/compound_rtcp_builder.cc new file mode 100644 index 00000000..b3cba68b --- /dev/null +++ b/cast/streaming/compound_rtcp_builder.cc @@ -0,0 +1,332 @@ +// 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_builder.h" + +#include <algorithm> +#include <iterator> +#include <limits> + +#include "cast/streaming/packet_util.h" +#include "cast/streaming/rtcp_session.h" +#include "util/integer_division.h" +#include "util/logging.h" +#include "util/std_util.h" + +using openscreen::AreElementsSortedAndUnique; +using openscreen::platform::Clock; + +namespace cast { +namespace streaming { + +CompoundRtcpBuilder::CompoundRtcpBuilder(RtcpSession* session) + : session_(session) { + OSP_DCHECK(session_); +} + +CompoundRtcpBuilder::~CompoundRtcpBuilder() = default; + +void CompoundRtcpBuilder::SetCheckpointFrame(FrameId frame_id) { + OSP_DCHECK_GE(frame_id, checkpoint_frame_id_); + checkpoint_frame_id_ = frame_id; +} + +void CompoundRtcpBuilder::SetPlayoutDelay(std::chrono::milliseconds delay) { + playout_delay_ = delay; +} + +void CompoundRtcpBuilder::SetPictureLossIndicator(bool picture_is_lost) { + picture_loss_indicator_ = picture_is_lost; +} + +void CompoundRtcpBuilder::IncludeReceiverReportInNextPacket( + const RtcpReportBlock& receiver_report) { + receiver_report_for_next_packet_ = receiver_report; +} + +void CompoundRtcpBuilder::IncludeFeedbackInNextPacket( + std::vector<PacketNack> packet_nacks, + std::vector<FrameId> frame_acks) { + // Note: Serialization of these lists will depend on the value of + // |checkpoint_frame_id_| when BuildPacket() is called later. + + nacks_for_next_packet_ = std::move(packet_nacks); + acks_for_next_packet_ = std::move(frame_acks); + +#if OSP_DCHECK_IS_ON() + OSP_DCHECK(AreElementsSortedAndUnique(nacks_for_next_packet_)); + OSP_DCHECK(AreElementsSortedAndUnique(acks_for_next_packet_)); + + // Consistency-check: An ACKed frame should not also be NACKed. + for (size_t ack_i = 0, nack_i = 0; ack_i < acks_for_next_packet_.size() && + nack_i < nacks_for_next_packet_.size();) { + const FrameId ack_frame_id = acks_for_next_packet_[ack_i]; + const FrameId nack_frame_id = nacks_for_next_packet_[nack_i].frame_id; + if (ack_frame_id < nack_frame_id) { + ++ack_i; + } else if (nack_frame_id < ack_frame_id) { + ++nack_i; + } else { + OSP_DCHECK_NE(ack_frame_id, nack_frame_id); + } + } + + // Redundancy-check: For any PacketNack whose packet ID is kAllPacketsLost, + // there should be no other PacketNack having the same FrameId. + for (size_t i = 1; i < nacks_for_next_packet_.size(); ++i) { + if (nacks_for_next_packet_[i].packet_id == kAllPacketsLost) { + // Since the elements are sorted, it's only necessary to check the + // immediately preceeding element to make sure it does not have the same + // FrameId. + OSP_DCHECK_NE(nacks_for_next_packet_[i].frame_id, + nacks_for_next_packet_[i - 1].frame_id); + } + } +#endif +} + +absl::Span<uint8_t> CompoundRtcpBuilder::BuildPacket( + Clock::time_point send_time, + absl::Span<uint8_t> buffer) { + OSP_CHECK_GE(buffer.size(), kRequiredBufferSize); + + uint8_t* const packet_begin = buffer.data(); + + // Receiver Report: Per RFC 3550, Section 6.4.2, all RTCP compound packets + // from receivers must include at least an empty receiver report at the start. + // It's not clear whether the Cast RTCP spec requires this, but it costs very + // little to do so. + AppendReceiverReportPacket(&buffer); + + // Receiver Reference Time Report: While this is optional in the Cast + // Streaming spec, it is always included by this implementation to improve the + // stability of the end-to-end system. + AppendReceiverReferenceTimeReportPacket(send_time, &buffer); + + // Picture Loss Indicator: Only included if the flag is currently set. + if (picture_loss_indicator_) { + AppendPictureLossIndicatorPacket(&buffer); + } + + // Cast Feedback: Checkpoint information, and add as many NACKs and ACKs as + // the remaning space available in the buffer will allow for. + AppendCastFeedbackPacket(&buffer); + + uint8_t* const packet_end = buffer.data(); + return absl::Span<uint8_t>(packet_begin, packet_end - packet_begin); +} + +void CompoundRtcpBuilder::AppendReceiverReportPacket( + absl::Span<uint8_t>* buffer) { + RtcpCommonHeader header; + header.packet_type = RtcpPacketType::kReceiverReport; + header.payload_size = kRtcpReceiverReportSize; + if (receiver_report_for_next_packet_) { + header.with.report_count = 1; + header.payload_size += kRtcpReportBlockSize; + } else { + header.with.report_count = 0; + } + header.AppendFields(buffer); + AppendField<uint32_t>(session_->receiver_ssrc(), buffer); + if (receiver_report_for_next_packet_) { + receiver_report_for_next_packet_->AppendFields(buffer); + receiver_report_for_next_packet_ = absl::nullopt; + } +} + +void CompoundRtcpBuilder::AppendReceiverReferenceTimeReportPacket( + Clock::time_point send_time, + absl::Span<uint8_t>* buffer) { + RtcpCommonHeader header; + header.packet_type = RtcpPacketType::kExtendedReports; + header.payload_size = kRtcpExtendedReportHeaderSize + + kRtcpExtendedReportBlockHeaderSize + + kRtcpReceiverReferenceTimeReportBlockSize; + header.AppendFields(buffer); + AppendField<uint32_t>(session_->receiver_ssrc(), buffer); + AppendField<uint8_t>(kRtcpReceiverReferenceTimeReportBlockType, buffer); + AppendField<uint8_t>(0 /* reserved/unused byte */, buffer); + AppendField<uint16_t>( + kRtcpReceiverReferenceTimeReportBlockSize / sizeof(uint32_t), buffer); + AppendField<uint64_t>(session_->ntp_converter().ToNtpTimestamp(send_time), + buffer); +} + +void CompoundRtcpBuilder::AppendPictureLossIndicatorPacket( + absl::Span<uint8_t>* buffer) { + RtcpCommonHeader header; + header.packet_type = RtcpPacketType::kPayloadSpecific; + header.with.subtype = RtcpSubtype::kPictureLossIndicator; + header.payload_size = kRtcpPictureLossIndicatorHeaderSize; + header.AppendFields(buffer); + AppendField<uint32_t>(session_->receiver_ssrc(), buffer); + AppendField<uint32_t>(session_->sender_ssrc(), buffer); +} + +void CompoundRtcpBuilder::AppendCastFeedbackPacket( + absl::Span<uint8_t>* buffer) { + // Reserve space for the RTCP Common Header. It will be serialized later, + // after the total size of the Cast Feedback message is known. + absl::Span<uint8_t> space_for_header = + ReserveSpace(kRtcpCommonHeaderSize, buffer); + uint8_t* const feedback_fields_begin = buffer->data(); + + // Append the mandatory fields. + AppendField<uint32_t>(session_->receiver_ssrc(), buffer); + AppendField<uint32_t>(session_->sender_ssrc(), buffer); + AppendField<uint32_t>(kRtcpCastIdentifierWord, buffer); + AppendField<uint8_t>(checkpoint_frame_id_.lower_8_bits(), buffer); + // The |loss_count_field| will be set after the Loss Fields are generated + // and the total count is known. + uint8_t* const loss_count_field = + ReserveSpace(sizeof(uint8_t), buffer).data(); + OSP_DCHECK_GT(playout_delay_.count(), 0); + OSP_DCHECK_LE(playout_delay_.count(), std::numeric_limits<uint16_t>::max()); + AppendField<uint16_t>(playout_delay_.count(), buffer); + + // Try to include as many Loss Fields as possible. Some of the NACKs might + // be dropped if the remaining space in the buffer is insufficient to + // include them all. + const int num_loss_fields = AppendCastFeedbackLossFields(buffer); + OSP_DCHECK_LE(num_loss_fields, std::numeric_limits<uint8_t>::max()); + *loss_count_field = num_loss_fields; + + // Try to include the CST2 header and ACK bit vector. Again, some of the + // ACKs might be dropped if the remaining space in the buffer is + // insufficient. + AppendCastFeedbackAckFields(buffer); + + // Go back and fill-in the header fields, now that the total size is known. + RtcpCommonHeader header; + header.packet_type = RtcpPacketType::kPayloadSpecific; + header.with.subtype = RtcpSubtype::kFeedback; + uint8_t* const feedback_fields_end = buffer->data(); + header.payload_size = feedback_fields_end - feedback_fields_begin; + header.AppendFields(&space_for_header); + + ++feedback_count_; +} + +int CompoundRtcpBuilder::AppendCastFeedbackLossFields( + absl::Span<uint8_t>* buffer) { + if (nacks_for_next_packet_.empty()) { + return 0; + } + + // The maximum number of entries is limited by available packet buffer space + // and the 8-bit |loss_count_field|. + const int max_num_loss_fields = + std::min<int>(buffer->size() / kRtcpFeedbackLossFieldSize, + std::numeric_limits<uint8_t>::max()); + + // Translate the |nacks_for_next_packet_| list into one or more entries + // representing specific packet losses. Omit any NACKs before the checkpoint. + OSP_DCHECK(AreElementsSortedAndUnique(nacks_for_next_packet_)); + auto it = + std::find_if(nacks_for_next_packet_.begin(), nacks_for_next_packet_.end(), + [this](const PacketNack& nack) { + return nack.frame_id > checkpoint_frame_id_; + }); + int num_loss_fields = 0; + while (it != nacks_for_next_packet_.end() && + num_loss_fields != max_num_loss_fields) { + const FrameId frame_id = it->frame_id; + const FramePacketId first_packet_id = it->packet_id; + uint8_t bit_vector = 0; + for (++it; it != nacks_for_next_packet_.end() && it->frame_id == frame_id; + ++it) { + const int shift = it->packet_id - first_packet_id - 1; + if (shift >= 8) { + break; + } + bit_vector |= 1 << shift; + } + AppendField<uint8_t>(frame_id.lower_8_bits(), buffer); + AppendField<uint16_t>(first_packet_id, buffer); + AppendField<uint8_t>(bit_vector, buffer); + ++num_loss_fields; + } + + nacks_for_next_packet_.clear(); + return num_loss_fields; +} + +void CompoundRtcpBuilder::AppendCastFeedbackAckFields( + absl::Span<uint8_t>* buffer) { + // Return if there is not enough space for the CST2 header and the + // smallest-possible ACK bit vector. + if (buffer->size() < + (kRtcpFeedbackAckHeaderSize + kRtcpMinAckBitVectorOctets)) { + return; + } + + // Write the CST2 header and reserve/initialize the start of the ACK bit + // vector. + AppendField<uint32_t>(kRtcpCst2IdentifierWord, buffer); + AppendField<uint8_t>(feedback_count_, buffer); + // The octet count field is set later, after the total is known. + uint8_t* const octet_count_field = + ReserveSpace(sizeof(uint8_t), buffer).data(); + // Start with the minimum required number of bit vector octets. + uint8_t* const ack_bitvector = + ReserveSpace(kRtcpMinAckBitVectorOctets, buffer).data(); + int num_ack_bitvector_octets = kRtcpMinAckBitVectorOctets; + memset(ack_bitvector, 0, kRtcpMinAckBitVectorOctets); + + // Set the bits of the ACK bit vector, auto-expanding the number of ACK octets + // if necessary (and while there is still room in the buffer). + if (!acks_for_next_packet_.empty()) { + OSP_DCHECK(AreElementsSortedAndUnique(acks_for_next_packet_)); + const FrameId first_frame_id = checkpoint_frame_id_ + 2; + for (const FrameId frame_id : acks_for_next_packet_) { + const int bit_index = frame_id - first_frame_id; + if (bit_index < 0) { + continue; + } + constexpr int kBitsPerOctet = 8; + const int octet_index = bit_index / kBitsPerOctet; + + // If needed, attempt to increase the number of ACK octets. + if (octet_index >= num_ack_bitvector_octets) { + // Compute how many additional octets are needed. + constexpr int kIncrement = sizeof(uint32_t); + const int num_additional = + openscreen::DividePositivesRoundingUp( + (octet_index + 1) - num_ack_bitvector_octets, kIncrement) * + kIncrement; + + // If there is not enough room in the buffer to add more ACKs, then do + // not continue. Also, if the new total count would exceed the design + // limit, do not continue. + if (static_cast<int>(buffer->size()) < num_additional) { + break; + } + const int new_count = num_ack_bitvector_octets + num_additional; + if (new_count > kRtcpMaxAckBitVectorOctets) { + break; + } + + // Reserve the additional space from the buffer, and initialize to zero. + memset(ReserveSpace(num_additional, buffer).data(), 0, num_additional); + num_ack_bitvector_octets = new_count; + } + + // At this point, the ACK bit vector is valid at |octet_index|. Set the + // bit representing the ACK for |frame_id|. + const int shift = bit_index % kBitsPerOctet; + ack_bitvector[octet_index] |= 1 << shift; + } + } + + // Now that the total size of the ACK bit vector is known, go back and set the + // octet count field. + OSP_DCHECK_LE(num_ack_bitvector_octets, std::numeric_limits<uint8_t>::max()); + *octet_count_field = num_ack_bitvector_octets; + + acks_for_next_packet_.clear(); +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/compound_rtcp_builder.h b/cast/streaming/compound_rtcp_builder.h new file mode 100644 index 00000000..58bc62fb --- /dev/null +++ b/cast/streaming/compound_rtcp_builder.h @@ -0,0 +1,140 @@ +// 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. + +#ifndef CAST_STREAMING_COMPOUND_RTCP_BUILDER_H_ +#define CAST_STREAMING_COMPOUND_RTCP_BUILDER_H_ + +#include <chrono> +#include <utility> +#include <vector> + +#include "absl/types/optional.h" +#include "absl/types/span.h" +#include "cast/streaming/constants.h" +#include "cast/streaming/frame_id.h" +#include "cast/streaming/rtcp_common.h" +#include "cast/streaming/rtp_defines.h" + +namespace cast { +namespace streaming { + +class RtcpSession; + +// Collects current status and feedback messages from the Receiver in the +// current process, and builds compound RTCP packets to be transmitted to a +// Sender. +// +// Usage: +// +// 1. Call the various SetXYZ/IncludeXYZInNextPacket() methods as the +// receiver's state changes. The SetXYZ() methods provide values that will +// be included in every RTCP packet until they are changed, while the +// IncludeXYZInNextPacket() methods provide values for only the next-built +// RTCP packet. The latter case is part of the overall protocol design, to +// help prevent the Sender from acting on stale Receiver state. +// +// 2. At certain times, call BuildPacket() and transmit it to the sender: +// a. By default, every 1/2 sec, to provide the sender with a "keep alive" +// ping that it can also use to monitor network round-trip times. +// b. When there is new feedback, the collected information should be +// immediately conveyed to the sender. +class CompoundRtcpBuilder { + public: + explicit CompoundRtcpBuilder(RtcpSession* session); + ~CompoundRtcpBuilder(); + + // Gets/Sets the checkpoint |frame_id| that will be included in built RTCP + // packets. This value indicates to the sender that all of the packets for all + // frames up to and including the given frame have been successfully received. + FrameId checkpoint_frame() const { return checkpoint_frame_id_; } + void SetCheckpointFrame(FrameId frame_id); + + // Gets/Sets the current end-to-end target playout delay setting for the Cast + // RTP receiver, to be included in built RTCP packets. This reflect any + // changes the sender has made by using the "Cast Adaptive Latency Extension" + // in received RTP packets. + std::chrono::milliseconds playout_delay() const { return playout_delay_; } + void SetPlayoutDelay(std::chrono::milliseconds delay); + + // Gets/Sets the picture loss indicator flag. While this is set, built RTCP + // packets will include a PLI message that indicates to the sender that there + // has been an unrecoverable decoding error. This asks the sender to provide a + // key frame as soon as possible. The client must explicitly clear this flag + // when decoding will recover. + bool is_picture_loss_indicator_set() const { return picture_loss_indicator_; } + void SetPictureLossIndicator(bool picture_is_lost); + + // Include a receiver report about recent packet receive activity in ONLY the + // next built RTCP packet. This replaces a prior receiver report if + // BuildPacket() was not called in the meantime (since only the most + // up-to-date version of the Receiver's state is relevant to the Sender). + void IncludeReceiverReportInNextPacket( + const RtcpReportBlock& receiver_report); + + // Include detailed feedback about wholly-received frames, whole missing + // frames, and partially-received frames (specific missing packets) in ONLY + // the next built RTCP packet. The data will be included in a best-effort + // fashion, depending on the size of the |buffer| passed to the next call to + // BuildPacket(). This replaces prior feedback data if BuildPacket() was not + // called in the meantime (since only the most up-to-date version of the + // Receiver's state is relevant to the Sender). + // + // The elements in the lists are assumed to be monotonically increasing: + // |packet_nacks| indicates specific packets that have not yet been received, + // or may use kAllPacketsLost to indicate that no packets have been received + // for a frame. |frame_acks| indicates which frames after the checkpoint frame + // have been fully received. + void IncludeFeedbackInNextPacket(std::vector<PacketNack> packet_nacks, + std::vector<FrameId> frame_acks); + + // Builds a compound RTCP packet and returns the portion of the |buffer| that + // was used. The buffer's size must be at least kRequiredBufferSize, but + // should generally be the maximum packet size (see discussion in + // rtp_defines.h), to avoid dropping any ACK/NACK feedback. + // + // |send_time| specifies the when the resulting packet will be sent. This + // should be monotonically increasing so the consuming side (the Sender) can + // determine the chronological ordering of RTCP packets. The Sender might also + // use this to estimate round-trip times over the network. + absl::Span<uint8_t> BuildPacket( + openscreen::platform::Clock::time_point send_time, + absl::Span<uint8_t> buffer); + + // The required buffer size to be provided to BuildPacket(). This accounts for + // all the possible headers and report structures that might be included, + // along with a reasonable amount of space for the feedback's ACK/NACKs bit + // vectors. + static constexpr int kRequiredBufferSize = 256; + + private: + // Helper methods called by BuildPacket() to append one RTCP packet to the + // |buffer| that will ultimately contain a "compound RTCP packet." + void AppendReceiverReportPacket(absl::Span<uint8_t>* buffer); + void AppendReceiverReferenceTimeReportPacket( + openscreen::platform::Clock::time_point send_time, + absl::Span<uint8_t>* buffer); + void AppendPictureLossIndicatorPacket(absl::Span<uint8_t>* buffer); + void AppendCastFeedbackPacket(absl::Span<uint8_t>* buffer); + int AppendCastFeedbackLossFields(absl::Span<uint8_t>* buffer); + void AppendCastFeedbackAckFields(absl::Span<uint8_t>* buffer); + + RtcpSession* const session_; + + // Data to include in the next built RTCP packet. + FrameId checkpoint_frame_id_ = FrameId::first() - 1; + std::chrono::milliseconds playout_delay_ = kDefaultTargetPlayoutDelay; + absl::optional<RtcpReportBlock> receiver_report_for_next_packet_; + std::vector<PacketNack> nacks_for_next_packet_; + std::vector<FrameId> acks_for_next_packet_; + bool picture_loss_indicator_ = false; + + // An 8-bit wrap-around counter that tracks how many times Cast Feedback has + // been included in the built RTCP packets. + uint8_t feedback_count_ = 0; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_COMPOUND_RTCP_BUILDER_H_ diff --git a/cast/streaming/compound_rtcp_builder_unittest.cc b/cast/streaming/compound_rtcp_builder_unittest.cc new file mode 100644 index 00000000..ab6f8c00 --- /dev/null +++ b/cast/streaming/compound_rtcp_builder_unittest.cc @@ -0,0 +1,373 @@ +// 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_builder.h" + +#include <algorithm> +#include <chrono> + +#include "cast/streaming/compound_rtcp_parser.h" +#include "cast/streaming/constants.h" +#include "cast/streaming/mock_compound_rtcp_parser_client.h" +#include "cast/streaming/rtcp_session.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "platform/api/time.h" + +using openscreen::platform::Clock; + +using testing::_; +using testing::Invoke; +using testing::Mock; +using testing::SaveArg; +using testing::StrictMock; + +namespace cast { +namespace streaming { +namespace { + +constexpr Ssrc kSenderSsrc{1}; +constexpr Ssrc kReceiverSsrc{2}; + +class CompoundRtcpBuilderTest : public testing::Test { + public: + RtcpSession* session() { return &session_; } + CompoundRtcpBuilder* builder() { return &builder_; } + StrictMock<MockCompoundRtcpParserClient>* client() { return &client_; } + CompoundRtcpParser* parser() { return &parser_; } + + // Return |timestamp| converted to the NtpTimestamp wire format and then + // converted back to the local Clock's time_point. The result will be either + // exactly equal to |original|, or one tick off from it due to the lossy + // conversions. + Clock::time_point ViaNtpTimestampTranslation( + Clock::time_point timestamp) const { + return session_.ntp_converter().ToLocalTime( + session_.ntp_converter().ToNtpTimestamp(timestamp)); + } + + private: + RtcpSession session_{kSenderSsrc, kReceiverSsrc, Clock::now()}; + CompoundRtcpBuilder builder_{&session_}; + StrictMock<MockCompoundRtcpParserClient> client_; + CompoundRtcpParser parser_{&session_, &client_}; +}; + +// Tests that the builder, by default, produces RTCP packets that always include +// the receiver's reference time and checkpoint information. +TEST_F(CompoundRtcpBuilderTest, TheBasics) { + const FrameId checkpoint = FrameId::first() + 42; + builder()->SetCheckpointFrame(checkpoint); + const std::chrono::milliseconds playout_delay{321}; + builder()->SetPlayoutDelay(playout_delay); + + const auto send_time = Clock::now(); + uint8_t buffer[CompoundRtcpBuilder::kRequiredBufferSize]; + const auto packet = builder()->BuildPacket(send_time, buffer); + ASSERT_TRUE(packet.data()); + + EXPECT_CALL(*(client()), OnReceiverReferenceTimeAdvanced( + ViaNtpTimestampTranslation(send_time))); + EXPECT_CALL(*(client()), OnReceiverCheckpoint(checkpoint, playout_delay)); + ASSERT_TRUE(parser()->Parse(packet, checkpoint)); +} + +// Tests that the builder correctly serializes a Receiver Report Block and +// includes it only in the next-built RTCP packet. +TEST_F(CompoundRtcpBuilderTest, WithReceiverReportBlock) { + const FrameId checkpoint = FrameId::first() + 42; + builder()->SetCheckpointFrame(checkpoint); + const auto playout_delay = builder()->playout_delay(); + + RtcpReportBlock original; + original.ssrc = kSenderSsrc; + original.packet_fraction_lost_numerator = 1; + original.cumulative_packets_lost = 2; + original.extended_high_sequence_number = 3; + original.jitter = RtpTimeDelta::FromTicks(4); + original.last_status_report_id = StatusReportId{0x05060708}; + original.delay_since_last_report = RtcpReportBlock::Delay(9); + builder()->IncludeReceiverReportInNextPacket(original); + + const auto send_time = Clock::now(); + uint8_t buffer[CompoundRtcpBuilder::kRequiredBufferSize]; + const auto packet = builder()->BuildPacket(send_time, buffer); + ASSERT_TRUE(packet.data()); + + // Expect that the builder has produced a RTCP packet that includes the + // receiver report block. + const auto max_feedback_frame_id = checkpoint + 2; + EXPECT_CALL(*(client()), OnReceiverReferenceTimeAdvanced( + ViaNtpTimestampTranslation(send_time))); + EXPECT_CALL(*(client()), OnReceiverCheckpoint(checkpoint, playout_delay)); + RtcpReportBlock parsed; + EXPECT_CALL(*(client()), OnReceiverReport(_)).WillOnce(SaveArg<0>(&parsed)); + ASSERT_TRUE(parser()->Parse(packet, max_feedback_frame_id)); + Mock::VerifyAndClearExpectations(client()); + EXPECT_EQ(original.ssrc, parsed.ssrc); + EXPECT_EQ(original.packet_fraction_lost_numerator, + parsed.packet_fraction_lost_numerator); + EXPECT_EQ(original.cumulative_packets_lost, parsed.cumulative_packets_lost); + EXPECT_EQ(original.extended_high_sequence_number, + parsed.extended_high_sequence_number); + EXPECT_EQ(original.jitter, parsed.jitter); + EXPECT_EQ(original.last_status_report_id, parsed.last_status_report_id); + EXPECT_EQ(original.delay_since_last_report, parsed.delay_since_last_report); + + // Build again, but this time the builder should not include the receiver + // report block. + const auto second_send_time = send_time + std::chrono::milliseconds(500); + const auto second_packet = builder()->BuildPacket(second_send_time, buffer); + ASSERT_TRUE(second_packet.data()); + EXPECT_CALL(*(client()), OnReceiverReferenceTimeAdvanced( + ViaNtpTimestampTranslation(second_send_time))); + EXPECT_CALL(*(client()), OnReceiverCheckpoint(checkpoint, playout_delay)); + EXPECT_CALL(*(client()), OnReceiverReport(_)).Times(0); + ASSERT_TRUE(parser()->Parse(second_packet, max_feedback_frame_id)); + Mock::VerifyAndClearExpectations(client()); +} + +// Tests that the builder repeatedly produces packets with the PLI message as +// long as the PLI flag is set, and produces packets without the PLI message +// while the flag is not set. +TEST_F(CompoundRtcpBuilderTest, WithPictureLossIndicator) { + // Turn the PLI flag off and on twice, generating several packets while the + // flag is in each state. + FrameId checkpoint = FrameId::first(); + auto send_time = Clock::now(); + uint8_t buffer[CompoundRtcpBuilder::kRequiredBufferSize]; + for (int status = 0; status <= 3; ++status) { + const bool pli_flag_set = ((status % 2) != 0); + builder()->SetPictureLossIndicator(pli_flag_set); + + // Produce three packets while the PLI flag is not changing, and confirm the + // PLI condition is being parsed on the other end. + for (int i = 0; i < 3; ++i) { + SCOPED_TRACE(testing::Message() << "status=" << status << ", i=" << i); + + EXPECT_EQ(pli_flag_set, builder()->is_picture_loss_indicator_set()); + builder()->SetCheckpointFrame(checkpoint); + const auto playout_delay = builder()->playout_delay(); + const auto packet = builder()->BuildPacket(send_time, buffer); + ASSERT_TRUE(packet.data()); + + const auto max_feedback_frame_id = checkpoint + 1; + EXPECT_CALL(*(client()), OnReceiverReferenceTimeAdvanced( + ViaNtpTimestampTranslation(send_time))); + EXPECT_CALL(*(client()), OnReceiverCheckpoint(checkpoint, playout_delay)); + EXPECT_CALL(*(client()), OnReceiverIndicatesPictureLoss()) + .Times(pli_flag_set ? 1 : 0); + ASSERT_TRUE(parser()->Parse(packet, max_feedback_frame_id)); + Mock::VerifyAndClearExpectations(client()); + + ++checkpoint; + send_time += std::chrono::milliseconds(500); + } + } +} + +// Tests that the builder produces packets with frame-level and specific-packet +// NACKs, but includes this information only in the next-built RTCP packet. +TEST_F(CompoundRtcpBuilderTest, WithNacks) { + const FrameId checkpoint = FrameId::first() + 15; + builder()->SetCheckpointFrame(checkpoint); + const auto playout_delay = builder()->playout_delay(); + + const std::vector<PacketNack> kPacketNacks = { + {FrameId::first() + 16, FramePacketId{0}}, + {FrameId::first() + 16, FramePacketId{1}}, + {FrameId::first() + 16, FramePacketId{2}}, + {FrameId::first() + 16, FramePacketId{7}}, + {FrameId::first() + 16, FramePacketId{15}}, + {FrameId::first() + 17, FramePacketId{19}}, + {FrameId::first() + 18, kAllPacketsLost}, + {FrameId::first() + 19, kAllPacketsLost}, + }; + builder()->IncludeFeedbackInNextPacket(kPacketNacks, {}); + + const auto send_time = Clock::now(); + uint8_t buffer[CompoundRtcpBuilder::kRequiredBufferSize]; + const auto packet = builder()->BuildPacket(send_time, buffer); + ASSERT_TRUE(packet.data()); + + // Expect that the builder has produced a RTCP packet that also includes the + // NACK feedback. + const auto kMaxFeedbackFrameId = FrameId::first() + 19; + EXPECT_CALL(*(client()), OnReceiverReferenceTimeAdvanced( + ViaNtpTimestampTranslation(send_time))); + EXPECT_CALL(*(client()), OnReceiverCheckpoint(checkpoint, playout_delay)); + EXPECT_CALL(*(client()), OnReceiverIsMissingPackets(kPacketNacks)); + ASSERT_TRUE(parser()->Parse(packet, kMaxFeedbackFrameId)); + Mock::VerifyAndClearExpectations(client()); + + // Build again, but this time the builder should not include the feedback. + const auto second_send_time = send_time + std::chrono::milliseconds(500); + const auto second_packet = builder()->BuildPacket(second_send_time, buffer); + ASSERT_TRUE(second_packet.data()); + EXPECT_CALL(*(client()), OnReceiverReferenceTimeAdvanced( + ViaNtpTimestampTranslation(second_send_time))); + EXPECT_CALL(*(client()), OnReceiverCheckpoint(checkpoint, playout_delay)); + EXPECT_CALL(*(client()), OnReceiverIsMissingPackets(_)).Times(0); + ASSERT_TRUE(parser()->Parse(second_packet, kMaxFeedbackFrameId)); + Mock::VerifyAndClearExpectations(client()); +} + +// Tests that the builder produces packets with frame-level ACKs, but includes +// this information only in the next-built RTCP packet. Both a single-frame ACK +// and a multi-frame ACK are tested, to exercise the various code paths +// containing the serialization logic that auto-extends the ACK bit vector +// length when necessary. +TEST_F(CompoundRtcpBuilderTest, WithAcks) { + const FrameId checkpoint = FrameId::first() + 22; + builder()->SetCheckpointFrame(checkpoint); + const auto playout_delay = builder()->playout_delay(); + + const std::vector<FrameId> kTestCases[] = { + // One frame ACK will result in building an ACK bit vector of 2 bytes + // only. + {FrameId::first() + 24}, + + // These frame ACKs were chosen so that the ACK bit vector must expand to + // be 6 (2 + 4) bytes long. + {FrameId::first() + 25, FrameId::first() + 42, FrameId::first() + 43}, + }; + const auto kMaxFeedbackFrameId = FrameId::first() + 50; + auto send_time = Clock::now(); + uint8_t buffer[CompoundRtcpBuilder::kRequiredBufferSize]; + for (const std::vector<FrameId>& frame_acks : kTestCases) { + // Include the frame ACK feedback, and expect that the builder will produce + // a RTCP packet that also includes the ACK feedback. + builder()->IncludeFeedbackInNextPacket({}, frame_acks); + const auto packet = builder()->BuildPacket(send_time, buffer); + ASSERT_TRUE(packet.data()); + EXPECT_CALL(*(client()), OnReceiverReferenceTimeAdvanced( + ViaNtpTimestampTranslation(send_time))); + EXPECT_CALL(*(client()), OnReceiverCheckpoint(checkpoint, playout_delay)); + EXPECT_CALL(*(client()), OnReceiverHasFrames(frame_acks)); + ASSERT_TRUE(parser()->Parse(packet, kMaxFeedbackFrameId)); + Mock::VerifyAndClearExpectations(client()); + + // Build again, but this time the builder should not include the feedback + // because it was already provided in the prior packet. + send_time += std::chrono::milliseconds(500); + const auto second_packet = builder()->BuildPacket(send_time, buffer); + ASSERT_TRUE(second_packet.data()); + EXPECT_CALL(*(client()), OnReceiverReferenceTimeAdvanced( + ViaNtpTimestampTranslation(send_time))); + EXPECT_CALL(*(client()), OnReceiverCheckpoint(checkpoint, playout_delay)); + EXPECT_CALL(*(client()), OnReceiverHasFrames(_)).Times(0); + ASSERT_TRUE(parser()->Parse(second_packet, kMaxFeedbackFrameId)); + Mock::VerifyAndClearExpectations(client()); + + send_time += std::chrono::milliseconds(500); + } +} + +// Tests that the builder handles scenarios where the provided buffer isn't big +// enough to hold all the ACK/NACK details. The expected behavior is that it +// will include as many of the NACKs as possible, followed by as many of the +// ACKs as possible. +TEST_F(CompoundRtcpBuilderTest, WithEverythingThatCanFit) { + const FrameId checkpoint = FrameId::first(); + builder()->SetCheckpointFrame(checkpoint); + + // For this test, use an abnormally-huge, but not impossible, list of NACKs + // and ACKs. Each NACK is for a separate frame so that a separate "loss field" + // will be generated in the serialized output. + std::vector<PacketNack> nacks; + for (FrameId f = checkpoint + 1; f != checkpoint + 64; ++f) { + nacks.push_back(PacketNack{f, FramePacketId{0}}); + } + std::vector<FrameId> acks; + for (FrameId f = checkpoint + 64; f < checkpoint + kMaxUnackedFrames; ++f) { + acks.push_back(f); + } + ASSERT_FALSE(acks.empty()); + + const auto max_feedback_frame_id = checkpoint + kMaxUnackedFrames; + + // First test: Include too many NACKs so that some of them will be dropped and + // none of the ACKs will be included. + builder()->IncludeFeedbackInNextPacket(nacks, acks); + uint8_t buffer[CompoundRtcpBuilder::kRequiredBufferSize]; + const auto packet = builder()->BuildPacket(Clock::now(), buffer); + ASSERT_TRUE(packet.data()); + EXPECT_EQ(sizeof(buffer), packet.size()); // The whole buffer should be used. + + EXPECT_CALL(*(client()), OnReceiverReferenceTimeAdvanced(_)); + EXPECT_CALL(*(client()), OnReceiverCheckpoint(checkpoint, _)); + // No ACKs could be included. + EXPECT_CALL(*(client()), OnReceiverHasFrames(_)).Times(0); + EXPECT_CALL(*(client()), OnReceiverIsMissingPackets(_)) + .WillOnce(Invoke([&](std::vector<PacketNack> parsed_nacks) { + // Some should be dropped. + ASSERT_LT(parsed_nacks.size(), nacks.size()); + EXPECT_TRUE(std::equal(parsed_nacks.begin(), parsed_nacks.end(), + nacks.begin())); + })); + ASSERT_TRUE(parser()->Parse(packet, max_feedback_frame_id)); + Mock::VerifyAndClearExpectations(client()); + + // Second test: Include fewer NACKs this time, so that none of the NACKs are + // dropped, but not all of the ACKs can be included. With internal knowledge + // of the wire format, it turns out that limiting serialization to 48 loss + // fields will free-up just enough space for 2 bytes of ACK bit vector. + constexpr int kFewerNackCount = 48; + builder()->IncludeFeedbackInNextPacket( + std::vector<PacketNack>(nacks.begin(), nacks.begin() + kFewerNackCount), + acks); + const auto second_packet = builder()->BuildPacket(Clock::now(), buffer); + ASSERT_TRUE(second_packet.data()); + // The whole buffer should be used. + EXPECT_EQ(sizeof(buffer), second_packet.size()); + + EXPECT_CALL(*(client()), OnReceiverReferenceTimeAdvanced(_)); + EXPECT_CALL(*(client()), OnReceiverCheckpoint(checkpoint, _)); + EXPECT_CALL(*(client()), OnReceiverHasFrames(_)) + .WillOnce(Invoke([&](std::vector<FrameId> parsed_acks) { + // Some of the ACKs should be dropped. + ASSERT_LT(parsed_acks.size(), acks.size()); + EXPECT_TRUE( + std::equal(parsed_acks.begin(), parsed_acks.end(), acks.begin())); + })); + EXPECT_CALL(*(client()), OnReceiverIsMissingPackets(_)) + .WillOnce(Invoke([&](absl::Span<const PacketNack> parsed_nacks) { + // All of the 48 NACKs provided should be present. + ASSERT_EQ(kFewerNackCount, static_cast<int>(parsed_nacks.size())); + EXPECT_TRUE(std::equal(parsed_nacks.begin(), parsed_nacks.end(), + nacks.begin())); + })); + ASSERT_TRUE(parser()->Parse(second_packet, max_feedback_frame_id)); + Mock::VerifyAndClearExpectations(client()); + + // Third test: Include even fewer NACKs, so that nothing is dropped. + constexpr int kEvenFewerNackCount = 46; + builder()->IncludeFeedbackInNextPacket( + std::vector<PacketNack>(nacks.begin(), + nacks.begin() + kEvenFewerNackCount), + acks); + const auto third_packet = builder()->BuildPacket(Clock::now(), buffer); + ASSERT_TRUE(third_packet.data()); + + EXPECT_CALL(*(client()), OnReceiverReferenceTimeAdvanced(_)); + EXPECT_CALL(*(client()), OnReceiverCheckpoint(checkpoint, _)); + EXPECT_CALL(*(client()), OnReceiverHasFrames(_)) + .WillOnce(Invoke([&](std::vector<FrameId> parsed_acks) { + // All acks should be present. + EXPECT_EQ(acks, parsed_acks); + })); + EXPECT_CALL(*(client()), OnReceiverIsMissingPackets(_)) + .WillOnce(Invoke([&](absl::Span<const PacketNack> parsed_nacks) { + // Only the first 46 NACKs provided should be present. + ASSERT_EQ(kEvenFewerNackCount, static_cast<int>(parsed_nacks.size())); + EXPECT_TRUE(std::equal(parsed_nacks.begin(), parsed_nacks.end(), + nacks.begin())); + })); + ASSERT_TRUE(parser()->Parse(third_packet, max_feedback_frame_id)); + Mock::VerifyAndClearExpectations(client()); +} + +} // namespace +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/compound_rtcp_parser.cc b/cast/streaming/compound_rtcp_parser.cc new file mode 100644 index 00000000..af5336ea --- /dev/null +++ b/cast/streaming/compound_rtcp_parser.cc @@ -0,0 +1,381 @@ +// 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/logging.h" +#include "util/std_util.h" + +using openscreen::platform::Clock; + +namespace cast { +namespace streaming { + +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(openscreen::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( + openscreen::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 streaming +} // namespace cast diff --git a/cast/streaming/compound_rtcp_parser.h b/cast/streaming/compound_rtcp_parser.h new file mode 100644 index 00000000..c9b71594 --- /dev/null +++ b/cast/streaming/compound_rtcp_parser.h @@ -0,0 +1,122 @@ +// 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. + +#ifndef CAST_STREAMING_COMPOUND_RTCP_PARSER_H_ +#define CAST_STREAMING_COMPOUND_RTCP_PARSER_H_ + +#include <chrono> +#include <vector> + +#include "absl/types/optional.h" +#include "absl/types/span.h" +#include "cast/streaming/frame_id.h" +#include "cast/streaming/rtcp_common.h" +#include "cast/streaming/rtp_defines.h" + +namespace cast { +namespace streaming { + +class RtcpSession; + +// Parses compound RTCP packets from a Receiver, invoking client callbacks when +// information of interest to a Sender (in the current process) is encountered. +class CompoundRtcpParser { + public: + // Callback interface used while parsing RTCP packets of interest to a Sender. + // The implementation must take into account: + // + // 1. Some/All of the data could be stale, as it only reflects the state of + // the Receiver at the time the packet was generated. A significant + // amount of time may have passed, depending on how long it took the + // packet to reach this local instance over the network. + // 2. The data shouldn't necessarily be trusted blindly: Some may be + // inconsistent (e.g., the same frame being ACKed and NACKed; or a frame + // that has not been sent yet is being NACKed). While that would indicate + // a badly-behaving Receiver, the Sender should be robust to such things. + class Client { + public: + Client(); + virtual ~Client(); + + // Called when a Receiver Reference Time Report has been parsed. + virtual void OnReceiverReferenceTimeAdvanced( + openscreen::platform::Clock::time_point reference_time); + + // Called when a Receiver Report with a Report Block has been parsed. + virtual void OnReceiverReport(const RtcpReportBlock& receiver_report); + + // Called when the Receiver has encountered an unrecoverable error in + // decoding the data. The Sender should provide a key frame as soon as + // possible. + virtual void OnReceiverIndicatesPictureLoss(); + + // Called when the Receiver indicates that all of the packets for all frames + // up to and including |frame_id| have been successfully received (or + // otherwise do not need to be re-transmitted). The |playout_delay| is the + // Receiver's current end-to-end target playout delay setting, which should + // reflect any changes the Sender has made by using the "Cast Adaptive + // Latency Extension" in RTP packets. + virtual void OnReceiverCheckpoint(FrameId frame_id, + std::chrono::milliseconds playout_delay); + + // Called to indicate the Receiver has successfully received all of the + // packets for each of the given |acks|. The argument's elements are in + // monotonically increasing order. + virtual void OnReceiverHasFrames(std::vector<FrameId> acks); + + // Called to indicate the Receiver is missing certain specific packets for + // certain specific frames. Any elements where the packet_id is + // kAllPacketsLost indicates that all the packets are missing for a frame. + // The argument's elements are in monotonically increasing order. + virtual void OnReceiverIsMissingPackets(std::vector<PacketNack> nacks); + }; + + // |session| and |client| must be non-null and must outlive the + // CompoundRtcpParser instance. + CompoundRtcpParser(RtcpSession* session, Client* client); + ~CompoundRtcpParser(); + + // Parses the packet, invoking the Client callback methods when appropriate. + // Returns true if the |packet| was well-formed, or false if it was corrupt. + // Note that none of the Client callback methods will be invoked until a + // packet is known to be well-formed. + // + // |max_feedback_frame_id| is the maximum-valued FrameId that could possibly + // be ACKnowledged by the Receiver, if there is Cast Feedback in the |packet|. + // This is needed for expanding truncated frame IDs correctly. + bool Parse(absl::Span<const uint8_t> packet, FrameId max_feedback_frame_id); + + private: + // These return true if the input was well-formed, and false if it was + // invalid/corrupt. The true/false value does NOT indicate whether the data + // contained within was ignored. Output arguments are only modified if the + // input contained the relevant field(s). + bool ParseReceiverReport(absl::Span<const uint8_t> in, + int num_report_blocks, + absl::optional<RtcpReportBlock>* receiver_report); + bool 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); + bool ParseExtendedReports( + absl::Span<const uint8_t> in, + openscreen::platform::Clock::time_point* receiver_reference_time); + bool ParsePictureLossIndicator(absl::Span<const uint8_t> in, + bool* picture_loss_indicator); + + RtcpSession* const session_; + Client* const client_; + + // Tracks the latest timestamp seen from any Receiver Reference Time Report, + // and uses this to ignore stale RTCP packets that arrived out-of-order and/or + // late from the network. + openscreen::platform::Clock::time_point latest_receiver_timestamp_; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_COMPOUND_RTCP_PARSER_H_ diff --git a/cast/streaming/compound_rtcp_parser_fuzzer.cc b/cast/streaming/compound_rtcp_parser_fuzzer.cc new file mode 100644 index 00000000..83290b0f --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer.cc @@ -0,0 +1,51 @@ +// 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 <stdint.h> + +#include "cast/streaming/compound_rtcp_parser.h" +#include "cast/streaming/frame_id.h" +#include "cast/streaming/rtcp_session.h" + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + using cast::streaming::CompoundRtcpParser; + using cast::streaming::FrameId; + using cast::streaming::RtcpSession; + using cast::streaming::Ssrc; + + constexpr Ssrc kSenderSsrcInSeedCorpus = 1; + constexpr Ssrc kReceiverSsrcInSeedCorpus = 2; + + // Allocate the RtcpSession and CompoundRtcpParser statically (i.e., one-time + // init) to improve the fuzzer's execution rate. This is because RtcpSession + // also contains a NtpTimeConverter, which samples the system clock at + // construction time. There is no reason to re-construct these objects for + // each fuzzer test input. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wexit-time-destructors" + static RtcpSession session(kSenderSsrcInSeedCorpus, + kReceiverSsrcInSeedCorpus); + static CompoundRtcpParser::Client client_that_ignores_everything; + static CompoundRtcpParser parser(&session, &client_that_ignores_everything); +#pragma clang diagnostic pop + + const auto max_feedback_frame_id = FrameId::first() + 100; + parser.Parse(absl::Span<const uint8_t>(data, size), max_feedback_frame_id); + + return 0; +} + +#if defined(NEEDS_MAIN_TO_CALL_FUZZER_DRIVER) + +// Forward declarations of Clang's built-in libFuzzer driver. +namespace fuzzer { +using TestOneInputCallback = int (*)(const uint8_t* data, size_t size); +int FuzzerDriver(int* argc, char*** argv, TestOneInputCallback callback); +} // namespace fuzzer + +int main(int argc, char* argv[]) { + return fuzzer::FuzzerDriver(&argc, &argv, LLVMFuzzerTestOneInput); +} + +#endif // defined(NEEDS_MAIN_TO_CALL_FUZZER_DRIVER) diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_basics.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_basics.bin Binary files differnew file mode 100644 index 00000000..9ab122b5 --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_basics.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_including_picture_loss_indicator.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_including_picture_loss_indicator.bin Binary files differnew file mode 100644 index 00000000..47a2cc9c --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_including_picture_loss_indicator.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_including_receiver_report_block.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_including_receiver_report_block.bin Binary files differnew file mode 100644 index 00000000..d9d0a544 --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_including_receiver_report_block.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_lots_of_nacks.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_lots_of_nacks.bin Binary files differnew file mode 100644 index 00000000..e3f89d66 --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_lots_of_nacks.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_lots_of_nacks_and_some_acks.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_lots_of_nacks_and_some_acks.bin Binary files differnew file mode 100644 index 00000000..102105c6 --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_lots_of_nacks_and_some_acks.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_lots_of_nacks_and_some_more_acks.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_lots_of_nacks_and_some_more_acks.bin Binary files differnew file mode 100644 index 00000000..80c5ae29 --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_lots_of_nacks_and_some_more_acks.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_multiple_acks.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_multiple_acks.bin Binary files differnew file mode 100644 index 00000000..59fd6c98 --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_multiple_acks.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_nack_mix.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_nack_mix.bin Binary files differnew file mode 100644 index 00000000..005cd37c --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_nack_mix.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_one_ack.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_one_ack.bin Binary files differnew file mode 100644 index 00000000..501338a8 --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_one_ack.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/feedback_with_nacks.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/feedback_with_nacks.bin Binary files differnew file mode 100644 index 00000000..b2689bdd --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/feedback_with_nacks.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/feedback_with_one_nack_and_one_ack.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/feedback_with_one_nack_and_one_ack.bin Binary files differnew file mode 100644 index 00000000..a41a9938 --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/feedback_with_one_nack_and_one_ack.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/feedback_with_zero_nacks_and_many_acks.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/feedback_with_zero_nacks_and_many_acks.bin Binary files differnew file mode 100644 index 00000000..7491fd64 --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/feedback_with_zero_nacks_and_many_acks.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/picture_loss_indicator.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/picture_loss_indicator.bin Binary files differnew file mode 100644 index 00000000..2c088ee1 --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/picture_loss_indicator.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/picture_loss_indicator_and_reference_time_report.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/picture_loss_indicator_and_reference_time_report.bin Binary files differnew file mode 100644 index 00000000..edbc6cf0 --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/picture_loss_indicator_and_reference_time_report.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/receiver_report_and_reference_time_report.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/receiver_report_and_reference_time_report.bin Binary files differnew file mode 100644 index 00000000..e9dc8b41 --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/receiver_report_and_reference_time_report.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/receiver_report_with_report_block.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/receiver_report_with_report_block.bin Binary files differnew file mode 100644 index 00000000..f67d4cc3 --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/receiver_report_with_report_block.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/receiver_report_without_report_block.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/receiver_report_without_report_block.bin Binary files differnew file mode 100644 index 00000000..04250612 --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/receiver_report_without_report_block.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/simple_feedback.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/simple_feedback.bin Binary files differnew file mode 100644 index 00000000..8e118ece --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/simple_feedback.bin diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/three_extended_reports.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/three_extended_reports.bin Binary files differnew file mode 100644 index 00000000..c513bb2f --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/three_extended_reports.bin diff --git a/cast/streaming/compound_rtcp_parser_unittest.cc b/cast/streaming/compound_rtcp_parser_unittest.cc new file mode 100644 index 00000000..863953af --- /dev/null +++ b/cast/streaming/compound_rtcp_parser_unittest.cc @@ -0,0 +1,408 @@ +// 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 <chrono> +#include <cmath> + +#include "cast/streaming/mock_compound_rtcp_parser_client.h" +#include "cast/streaming/rtcp_session.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "platform/api/time.h" + +using testing::_; +using testing::Mock; +using testing::SaveArg; +using testing::StrictMock; + +namespace cast { +namespace streaming { +namespace { + +constexpr Ssrc kSenderSsrc{1}; +constexpr Ssrc kReceiverSsrc{2}; + +class CompoundRtcpParserTest : public testing::Test { + public: + RtcpSession* session() { return &session_; } + StrictMock<MockCompoundRtcpParserClient>* client() { return &client_; } + CompoundRtcpParser* parser() { return &parser_; } + + private: + RtcpSession session_{kSenderSsrc, kReceiverSsrc, + openscreen::platform::Clock::now()}; + StrictMock<MockCompoundRtcpParserClient> client_; + CompoundRtcpParser parser_{&session_, &client_}; +}; + +TEST_F(CompoundRtcpParserTest, ProcessesEmptyPacket) { + const uint8_t kEmpty[0] = {}; + // Expect NO calls to mock client. + EXPECT_TRUE( + parser()->Parse(absl::Span<const uint8_t>(kEmpty, 0), FrameId::first())); +} + +TEST_F(CompoundRtcpParserTest, ReturnsErrorForGarbage) { + const uint8_t kGarbage[] = { + 0x42, 0x61, 0x16, 0x17, 0x26, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, + 0x6e, 0x67, 0x2f, 0x63, 0x61, 0x73, 0x74, 0x2f, 0x63, 0x6f, 0x6d, 0x70, + 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x72, 0x74, 0x63, 0x70, 0x5f}; + // Expect NO calls to mock client. + EXPECT_FALSE(parser()->Parse(kGarbage, FrameId::first())); +} + +TEST_F(CompoundRtcpParserTest, ParsesReceiverReportWithoutReportBlock) { + // clang-format off + const uint8_t kReceiverReportWithoutReportBlock[] = { + 0b10000000, // Version=2, Padding=no, ReportCount=0. + 201, // RTCP Packet type byte. + 0x00, 0x01, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x02, // Receiver SSRC. + }; + // clang-format on + + // Expect NO calls to mock client. + EXPECT_TRUE( + parser()->Parse(kReceiverReportWithoutReportBlock, FrameId::first())); +} + +TEST_F(CompoundRtcpParserTest, ParsesReceiverReportWithReportBlock) { + // clang-format off + const uint8_t kReceiverReportWithReportBlock[] = { + 0b10000001, // Version=2, Padding=no, ReportCount=1. + 201, // RTCP Packet type byte. + 0x00, 0x07, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x02, // Receiver SSRC. + + // Report block: + 0x00, 0x00, 0x00, 0x01, // Sender SSRC. + 0x05, // Fraction Lost. + 0x01, 0x02, 0x03, // Cumulative # packets lost. + 0x09, 0x09, 0x09, 0x02, // Highest sequence number. + 0x00, 0x00, 0x00, 0xaa, // Interarrival Jitter. + 0x0b, 0x0c, 0x8f, 0xed, // Sender Report ID. + 0x00, 0x01, 0x00, 0x00, // Delay since last sender report. + }; + // clang-format on + + RtcpReportBlock block; + EXPECT_CALL(*(client()), OnReceiverReport(_)).WillOnce(SaveArg<0>(&block)); + EXPECT_TRUE( + parser()->Parse(kReceiverReportWithReportBlock, FrameId::first())); + Mock::VerifyAndClearExpectations(client()); + EXPECT_EQ(kSenderSsrc, block.ssrc); + EXPECT_EQ(uint8_t{5}, block.packet_fraction_lost_numerator); + EXPECT_EQ(0x010203, block.cumulative_packets_lost); + EXPECT_EQ(uint32_t{0x09090902}, block.extended_high_sequence_number); + EXPECT_EQ(RtpTimeDelta::FromTicks(170), block.jitter); + EXPECT_EQ(StatusReportId{0x0b0c8fed}, block.last_status_report_id); + EXPECT_EQ(RtcpReportBlock::Delay(65536), block.delay_since_last_report); +} + +TEST_F(CompoundRtcpParserTest, ParsesPictureLossIndicatorMessage) { + // clang-format off + const uint8_t kPictureLossIndicatorPacket[] = { + 0b10000000 | 1, // Version=2, Padding=no, Subtype=PLI. + 206, // RTCP Packet type byte. + 0x00, 0x02, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x02, // Receiver SSRC. + 0x00, 0x00, 0x00, 0x01, // Sender SSRC. + }; + + const uint8_t kPictureLossIndicatorPacketWithWrongReceiverSsrc[] = { + 0b10000000 | 1, // Version=2, Padding=no, Subtype=PLI. + 206, // RTCP Packet type byte. + 0x00, 0x02, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x03, // WRONG Receiver SSRC. + 0x00, 0x00, 0x00, 0x01, // Sender SSRC. + }; + + const uint8_t kPictureLossIndicatorPacketWithWrongSenderSsrc[] = { + 0b10000000 | 1, // Version=2, Padding=no, Subtype=PLI. + 206, // RTCP Packet type byte. + 0x00, 0x02, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x02, // Receiver SSRC. + 0x00, 0x00, 0x00, 0x03, // WRONG Sender SSRC. + }; + // clang-format on + + // The mock client should get a PLI notification when the packet is valid and + // contains the correct SSRCs. + EXPECT_CALL(*(client()), OnReceiverIndicatesPictureLoss()); + EXPECT_TRUE(parser()->Parse(kPictureLossIndicatorPacket, FrameId::first())); + Mock::VerifyAndClearExpectations(client()); + + // The mock client should get no PLI notifications when either of the SSRCs is + // incorrect. + EXPECT_CALL(*(client()), OnReceiverIndicatesPictureLoss()).Times(0); + EXPECT_TRUE(parser()->Parse(kPictureLossIndicatorPacketWithWrongReceiverSsrc, + FrameId::first())); + Mock::VerifyAndClearExpectations(client()); + EXPECT_CALL(*(client()), OnReceiverIndicatesPictureLoss()).Times(0); + EXPECT_TRUE(parser()->Parse(kPictureLossIndicatorPacketWithWrongSenderSsrc, + FrameId::first())); + Mock::VerifyAndClearExpectations(client()); +} + +// Tests that RTCP packets containing chronologically-old data are ignored. This +// test's methodology simulates a real-world possibility: A receiver sends a +// "Picture Loss Indicator" in one RTCP packet, and then it sends another packet +// ~1 second later without the PLI, indicating the problem has been resolved. +// However, the packets are delivered out-of-order by the network. In this case, +// the CompoundRtcpParser should ignore the stale packet containing the PLI. +TEST_F(CompoundRtcpParserTest, IgnoresStalePackets) { + // clang-format off + const uint8_t kNotStaleCompoundPacket[] = { + // Receiver report: + 0b10000000, // Version=2, Padding=no, ReportCount=0. + 201, // RTCP Packet type byte. + 0x00, 0x01, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x02, // Receiver SSRC. + + // Receiver reference time report: + 0b10000000, // Version=2, Padding=no. + 207, // RTCP Packet type byte. + 0x00, 0x04, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x02, // Receiver SSRC. + 0x04, // Block type = Receiver Reference Time Report + 0x00, // Reserved byte. + 0x00, 0x02, // Block length = 2. + 0xe0, 0x73, 0x2e, 0x54, // NTP Timestamp (late evening on 2019-04-30). + 0x80, 0x00, 0x00, 0x00, + }; + + const uint8_t kStaleCompoundPacketWithPli[] = { + // Picture loss indicator: + 0b10000000 | 1, // Version=2, Padding=no, Subtype=PLI. + 206, // RTCP Packet type byte. + 0x00, 0x02, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x02, // Receiver SSRC. + 0x00, 0x00, 0x00, 0x01, // Sender SSRC. + + // Receiver reference time report: + 0b10000000, // Version=2, Padding=no. + 207, // RTCP Packet type byte. + 0x00, 0x04, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x02, // Receiver SSRC. + 0x04, // Block type = Receiver Reference Time Report + 0x00, // Reserved byte. + 0x00, 0x02, // Block length = 2. + 0xe0, 0x73, 0x2e, 0x53, // NTP Timestamp (late evening on 2019-04-30). + 0x42, 0x31, 0x20, 0x00, + }; + // clang-format on + + const auto expected_timestamp = + session()->ntp_converter().ToLocalTime(NtpTimestamp{0xe0732e5480000000}); + EXPECT_CALL(*(client()), OnReceiverReferenceTimeAdvanced(expected_timestamp)); + EXPECT_CALL(*(client()), OnReceiverIndicatesPictureLoss()).Times(0); + EXPECT_TRUE(parser()->Parse(kNotStaleCompoundPacket, FrameId::first())); + EXPECT_TRUE(parser()->Parse(kStaleCompoundPacketWithPli, FrameId::first())); +} + +// Tests that unknown RTCP extended reports are ignored, but known ones are +// still parsed when sent alongside the unknown ones. +TEST_F(CompoundRtcpParserTest, IgnoresUnknownExtendedReports) { + // clang-format off + const uint8_t kPacketWithThreeExtendedReports[] = { + 0b10000000, // Version=2, Padding=no. + 207, // RTCP Packet type byte. + 0x00, 0x0c, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x02, // Receiver SSRC. + + // Unknown extended report: + 0x02, // Block type = unknown (2) + 0x00, // Reserved byte. + 0x00, 0x06, // Block length = 6 words. + 0x01, 0x01, 0x01, 0x01, + 0x02, 0x02, 0x02, 0x02, + 0x03, 0x03, 0x03, 0x03, + 0x04, 0x04, 0x04, 0x04, + 0x05, 0x05, 0x05, 0x05, + 0x06, 0x06, 0x06, 0x06, + + // Receiver Reference Time Report: + 0x04, // Block type = RRTR + 0x00, // Reserved byte. + 0x00, 0x02, // Block length = 2 words. + 0xe0, 0x73, 0x2e, 0x55, // NTP Timestamp (late evening on 2019-04-30). + 0x00, 0x00, 0x00, 0x00, + + // Another unknown extended report: + 0x00, // Block type = unknown (0) + 0x00, // Reserved byte. + 0x00, 0x00, // Block length = 0 words. + }; + // clang-format on + + const auto expected_timestamp = + session()->ntp_converter().ToLocalTime(NtpTimestamp{0xe0732e5500000000}); + EXPECT_CALL(*(client()), OnReceiverReferenceTimeAdvanced(expected_timestamp)); + EXPECT_TRUE( + parser()->Parse(kPacketWithThreeExtendedReports, FrameId::first())); +} + +// Tests that a simple Cast Feedback packet is parsed, and the checkpoint frame +// ID is properly bit-extended, based on the current state of the Sender. +TEST_F(CompoundRtcpParserTest, ParsesSimpleFeedback) { + // clang-format off + const uint8_t kFeedbackPacket[] = { + 0b10000000 | 15, // Version=2, Padding=no, Subtype=Feedback. + 206, // RTCP Packet type byte. + 0x00, 0x04, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x02, // Receiver SSRC. + 0x00, 0x00, 0x00, 0x01, // Sender SSRC. + 'C', 'A', 'S', 'T', + 0x0a, // Checkpoint Frame ID = 10. + 0x00, // No NACKs. + 0x02, 0x26, // Playout delay = 550 ms. + }; + // clang-format on + + // First scenario: Valid range of FrameIds is [0,42]. + const auto kMaxFeedbackFrameId0 = FrameId::first() + 42; + const auto expected_frame_id0 = FrameId::first() + 10; + const auto expected_playout_delay = std::chrono::milliseconds(550); + EXPECT_CALL(*(client()), + OnReceiverCheckpoint(expected_frame_id0, expected_playout_delay)); + EXPECT_TRUE(parser()->Parse(kFeedbackPacket, kMaxFeedbackFrameId0)); + Mock::VerifyAndClearExpectations(client()); + + // Second scenario: Valid range of FrameIds is [299,554]. Note: 544 == 0x22a. + const auto kMaxFeedbackFrameId1 = FrameId::first() + 0x22a; + const auto expected_frame_id1 = FrameId::first() + 0x20a; + EXPECT_CALL(*(client()), + OnReceiverCheckpoint(expected_frame_id1, expected_playout_delay)); + EXPECT_TRUE(parser()->Parse(kFeedbackPacket, kMaxFeedbackFrameId1)); + Mock::VerifyAndClearExpectations(client()); +} + +// Tests NACK feedback parsing, and that redundant NACKs are de-duped, and that +// the results are delivered to the client sorted. +TEST_F(CompoundRtcpParserTest, ParsesFeedbackWithNacks) { + // clang-format off + const uint8_t kFeedbackPacket[] = { + 0b10000000 | 15, // Version=2, Padding=no, Subtype=Feedback. + 206, // RTCP Packet type byte. + 0x00, 0x0b, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x02, // Receiver SSRC. + 0x00, 0x00, 0x00, 0x01, // Sender SSRC. + 'C', 'A', 'S', 'T', + 0x0a, // Checkpoint Frame ID = 10. + 0x07, // Seven NACKs. + 0x02, 0x28, // Playout delay = 552 ms. + 0x0b, 0x00, 0x03, 0b00000000, // NACK Packet 3 in Frame 11. + 0x0b, 0x00, 0x07, 0b10001101, // NACK Packet 7-8, 10-11, 15 in Frame 11. + 0x0d, 0xff, 0xff, 0b00000000, // NACK all packets in Frame 13. + 0x0b, 0x00, 0x0b, 0b00000000, // Redundant: NACK packet 11 in Frame 11. + 0x0c, 0xff, 0xff, 0b00000000, // NACK all packets in Frame 12. + 0x0d, 0x00, 0x01, 0b00000000, // Redundant: NACK packet 1 in Frame 13. + 0x0e, 0x00, 0x00, 0b01000010, // NACK packets 0, 2, 7 in Frame 14. + }; + // clang-format on + + // The de-duped and sorted list of the frame/packet NACKs expected when + // parsing kFeedbackPacket: + const std::vector<PacketNack> kMissingPackets = { + {FrameId::first() + 11, 3}, + {FrameId::first() + 11, 7}, + {FrameId::first() + 11, 8}, + {FrameId::first() + 11, 10}, + {FrameId::first() + 11, 11}, + {FrameId::first() + 11, 15}, + {FrameId::first() + 12, kAllPacketsLost}, + {FrameId::first() + 13, kAllPacketsLost}, + {FrameId::first() + 14, 0}, + {FrameId::first() + 14, 2}, + {FrameId::first() + 14, 7}, + }; + + const auto kMaxFeedbackFrameId = FrameId::first() + 42; + const auto expected_frame_id = FrameId::first() + 10; + const auto expected_playout_delay = std::chrono::milliseconds(552); + EXPECT_CALL(*(client()), + OnReceiverCheckpoint(expected_frame_id, expected_playout_delay)); + EXPECT_CALL(*(client()), OnReceiverIsMissingPackets(kMissingPackets)); + EXPECT_TRUE(parser()->Parse(kFeedbackPacket, kMaxFeedbackFrameId)); +} + +// Tests the CST2 "later frame ACK" parsing: Both the common "2 bytes of bit +// vector" case, and a "multiple words of bit vector" case. +TEST_F(CompoundRtcpParserTest, ParsesFeedbackWithAcks) { + // clang-format off + const uint8_t kSmallerFeedbackPacket[] = { + 0b10000000 | 15, // Version=2, Padding=no, Subtype=Feedback. + 206, // RTCP Packet type byte. + 0x00, 0x07, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x02, // Receiver SSRC. + 0x00, 0x00, 0x00, 0x01, // Sender SSRC. + 'C', 'A', 'S', 'T', + 0x0a, // Checkpoint Frame ID = 10. + 0x01, // One NACK. + 0x01, 0x26, // Playout delay = 294 ms. + 0x0b, 0x00, 0x03, 0b00000000, // NACK Packet 3 in Frame 11. + 'C', 'S', 'T', '2', + 0x99, // Feedback counter. + 0x02, // 2 bytes of ACK bit vector. + 0b00000010, 0b00000000, // ACK only frame 13. + }; + + const uint8_t kLargerFeedbackPacket[] = { + 0b10000000 | 15, // Version=2, Padding=no, Subtype=Feedback. + 206, // RTCP Packet type byte. + 0x00, 0x08, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x02, // Receiver SSRC. + 0x00, 0x00, 0x00, 0x01, // Sender SSRC. + 'C', 'A', 'S', 'T', + 0x0a, // Checkpoint Frame ID = 10. + 0x00, // Zero NACKs. + 0x01, 0x26, // Playout delay = 294 ms. + 'C', 'S', 'T', '2', + 0x99, // Feedback counter. + 0x0a, // 10 bytes of ACK bit vector. + 0b11111111, 0b11111111, // ACK frames 12-27. + 0b00000000, 0b00000001, 0b00000000, 0b00000000, // ACK frame 36. + 0b00000000, 0b00000000, 0b00000000, 0b10000000, // ACK frame 91. + }; + // clang-format on + + // From the smaller packet: The single frame ACK and single packet NACK. + const std::vector<FrameId> kFrame13Only = {FrameId::first() + 13}; + const std::vector<PacketNack> kFrame11Packet3Only = { + {FrameId::first() + 11, 3}}; + + // From the larger packet: Many frame ACKs. + const std::vector<FrameId> kManyFrames = { + FrameId::first() + 12, FrameId::first() + 13, FrameId::first() + 14, + FrameId::first() + 15, FrameId::first() + 16, FrameId::first() + 17, + FrameId::first() + 18, FrameId::first() + 19, FrameId::first() + 20, + FrameId::first() + 21, FrameId::first() + 22, FrameId::first() + 23, + FrameId::first() + 24, FrameId::first() + 25, FrameId::first() + 26, + FrameId::first() + 27, FrameId::first() + 36, FrameId::first() + 91, + }; + + // Test the smaller packet. + const auto kMaxFeedbackFrameId = FrameId::first() + 100; + const auto expected_frame_id = FrameId::first() + 10; + const auto expected_playout_delay = std::chrono::milliseconds(294); + EXPECT_CALL(*(client()), + OnReceiverCheckpoint(expected_frame_id, expected_playout_delay)); + EXPECT_CALL(*(client()), OnReceiverHasFrames(kFrame13Only)); + EXPECT_CALL(*(client()), OnReceiverIsMissingPackets(kFrame11Packet3Only)); + EXPECT_TRUE(parser()->Parse(kSmallerFeedbackPacket, kMaxFeedbackFrameId)); + Mock::VerifyAndClearExpectations(client()); + + // Test the larger ACK packet. + EXPECT_CALL(*(client()), + OnReceiverCheckpoint(expected_frame_id, expected_playout_delay)); + EXPECT_CALL(*(client()), OnReceiverHasFrames(kManyFrames)); + EXPECT_TRUE(parser()->Parse(kLargerFeedbackPacket, kMaxFeedbackFrameId)); + Mock::VerifyAndClearExpectations(client()); +} + +} // namespace +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/constants.h b/cast/streaming/constants.h new file mode 100644 index 00000000..d67ba3e9 --- /dev/null +++ b/cast/streaming/constants.h @@ -0,0 +1,44 @@ +// Copyright 2015 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. + +#ifndef CAST_STREAMING_CONSTANTS_H_ +#define CAST_STREAMING_CONSTANTS_H_ + +//////////////////////////////////////////////////////////////////////////////// +// NOTE: This file should only contain constants that are reasonably globally +// used (i.e., by many modules, and in all or nearly all subdirs). Do NOT add +// non-POD constants, functions, interfaces, or any logic to this module. +//////////////////////////////////////////////////////////////////////////////// + +#include <chrono> +#include <ratio> + +namespace cast { +namespace streaming { + +// Default target playout delay. The playout delay is the window of time between +// capture from the source until presentation at the receiver. +constexpr std::chrono::milliseconds kDefaultTargetPlayoutDelay(400); + +// Target number of milliseconds between the sending of RTCP reports. Both +// senders and receivers regularly send RTCP reports to their peer. +constexpr std::chrono::milliseconds kRtcpReportInterval(500); + +// This is an important system-wide constant. This limits how much history +// the implementation must retain in order to process the acknowledgements of +// past frames. +// +// This value is carefully choosen such that it fits in the 8-bits range for +// frame IDs. It is also less than half of the full 8-bits range such that +// logic can handle wrap around and compare two frame IDs meaningfully. +constexpr int kMaxUnackedFrames = 120; + +// The spec declares RTP timestamps must always have a timebase of 90000 ticks +// per second for video. +using kVideoTimebase = std::ratio<1, 90000>; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_CONSTANTS_H_ diff --git a/cast/streaming/encoded_frame.cc b/cast/streaming/encoded_frame.cc new file mode 100644 index 00000000..ebd6cc75 --- /dev/null +++ b/cast/streaming/encoded_frame.cc @@ -0,0 +1,26 @@ +// Copyright 2014 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/encoded_frame.h" + +namespace cast { +namespace streaming { + +EncodedFrame::EncodedFrame() = default; +EncodedFrame::~EncodedFrame() = default; + +EncodedFrame::EncodedFrame(EncodedFrame&&) MAYBE_NOEXCEPT = default; +EncodedFrame& EncodedFrame::operator=(EncodedFrame&&) MAYBE_NOEXCEPT = default; + +void EncodedFrame::CopyMetadataTo(EncodedFrame* dest) const { + dest->dependency = this->dependency; + dest->frame_id = this->frame_id; + dest->referenced_frame_id = this->referenced_frame_id; + dest->rtp_timestamp = this->rtp_timestamp; + dest->reference_time = this->reference_time; + dest->new_playout_delay = this->new_playout_delay; +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/encoded_frame.h b/cast/streaming/encoded_frame.h new file mode 100644 index 00000000..b42b6340 --- /dev/null +++ b/cast/streaming/encoded_frame.h @@ -0,0 +1,99 @@ +// Copyright 2014 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. + +#ifndef CAST_STREAMING_ENCODED_FRAME_H_ +#define CAST_STREAMING_ENCODED_FRAME_H_ + +#include <stdint.h> + +#include <chrono> +#include <vector> + +#include "absl/types/span.h" +#include "cast/streaming/frame_id.h" +#include "cast/streaming/rtp_time.h" +#include "platform/api/time.h" +#include "platform/base/macros.h" + +namespace cast { +namespace streaming { + +// A combination of metadata and data for one encoded frame. This can contain +// audio data or video data or other. +struct EncodedFrame { + enum Dependency : int8_t { + // "null" value, used to indicate whether |dependency| has been set. + UNKNOWN_DEPENDENCY, + + // Not decodable without the reference frame indicated by + // |referenced_frame_id|. + DEPENDS_ON_ANOTHER, + + // Independently decodable. + INDEPENDENTLY_DECODABLE, + + // Independently decodable, and no future frames will depend on any frames + // before this one. + KEY_FRAME, + }; + + EncodedFrame(); + ~EncodedFrame(); + + EncodedFrame(EncodedFrame&&) MAYBE_NOEXCEPT; + EncodedFrame& operator=(EncodedFrame&&) MAYBE_NOEXCEPT; + + // Copies all members except |data| to |dest|. Does not modify |dest->data|. + void CopyMetadataTo(EncodedFrame* dest) const; + + // This frame's dependency relationship with respect to other frames. + Dependency dependency = UNKNOWN_DEPENDENCY; + + // The label associated with this frame. Implies an ordering relative to + // other frames in the same stream. + FrameId frame_id; + + // The label associated with the frame upon which this frame depends. If + // this frame does not require any other frame in order to become decodable + // (e.g., key frames), |referenced_frame_id| must equal |frame_id|. + FrameId referenced_frame_id; + + // The stream timestamp, on the timeline of the signal data. For example, RTP + // timestamps for audio are usually defined as the total number of audio + // samples encoded in all prior frames. A playback system uses this value to + // detect gaps in the stream, and otherwise stretch the signal to gradually + // re-align towards playout targets when too much drift has occurred (see + // |reference_time|, below). + RtpTimeTicks rtp_timestamp; + + // The common reference clock timestamp for this frame. Over a sequence of + // frames, this time value is expected to drift with respect to the elapsed + // time implied by the RTP timestamps; and this may not necessarily increment + // with precise regularity. + // + // This value originates from a sender, and is the time at which the frame was + // captured/recorded. In the receiver context, this value is the computed + // target playout time, which is used for guiding the timing of presentation + // (see |rtp_timestamp|, above). It is also meant to be used to synchronize + // the presentation of multiple streams (e.g., audio and video), commonly + // known as "lip-sync." It is NOT meant to be a mandatory/exact playout time. + openscreen::platform::Clock::time_point reference_time; + + // Playout delay for this and all future frames. Used by the Adaptive + // Playout delay extension. Non-positive values means no change. + std::chrono::milliseconds new_playout_delay{}; + + // Pointer to a buffer containing the encoded signal data for the frame. In + // the sender context, this points to the data to be sent, and nothing will be + // mutated. In the receiver context, this is set to the region of a + // client-provided buffer that was populated. + absl::Span<uint8_t> data; + + OSP_DISALLOW_COPY_AND_ASSIGN(EncodedFrame); +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_ENCODED_FRAME_H_ diff --git a/cast/streaming/environment.cc b/cast/streaming/environment.cc new file mode 100644 index 00000000..954a2e0c --- /dev/null +++ b/cast/streaming/environment.cc @@ -0,0 +1,140 @@ +// 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/environment.h" + +#include "cast/streaming/rtp_defines.h" +#include "platform/api/task_runner.h" +#include "util/logging.h" + +using openscreen::Error; +using openscreen::ErrorOr; +using openscreen::IPAddress; +using openscreen::IPEndpoint; +using openscreen::platform::Clock; +using openscreen::platform::ClockNowFunctionPtr; +using openscreen::platform::TaskRunner; +using openscreen::platform::UdpPacket; +using openscreen::platform::UdpSocket; + +namespace cast { +namespace streaming { + +Environment::Environment(ClockNowFunctionPtr now_function, + TaskRunner* task_runner) + : now_function_(now_function), task_runner_(task_runner) { + OSP_DCHECK(now_function_); + OSP_DCHECK(task_runner_); +} + +Environment::Environment(ClockNowFunctionPtr now_function, + TaskRunner* task_runner, + const IPEndpoint& local_endpoint) + : Environment(now_function, task_runner) { + ErrorOr<std::unique_ptr<UdpSocket>> result = + UdpSocket::Create(task_runner_, this, local_endpoint); + const_cast<std::unique_ptr<UdpSocket>&>(socket_) = std::move(result.value()); + if (socket_) { + socket_->Bind(); + } else { + OSP_LOG_ERROR << "Unable to create a UDP socket bound to " << local_endpoint + << ": " << result.error(); + } +} + +Environment::~Environment() = default; + +IPEndpoint Environment::GetBoundLocalEndpoint() const { + if (socket_) { + return socket_->GetLocalEndpoint(); + } + return IPEndpoint{}; +} + +void Environment::ConsumeIncomingPackets(PacketConsumer* packet_consumer) { + OSP_DCHECK(packet_consumer); + OSP_DCHECK(!packet_consumer_); + packet_consumer_ = packet_consumer; +} + +void Environment::DropIncomingPackets() { + packet_consumer_ = nullptr; +} + +int Environment::GetMaxPacketSize() const { + // Return hard-coded values for UDP over wired Ethernet (which is a smaller + // MTU than typical defaults for UDP over 802.11 wireless). Performance would + // be more-optimized if the network were probed for the actual value. See + // discussion in rtp_defines.h. + switch (remote_endpoint_.address.version()) { + case IPAddress::Version::kV4: + return kMaxRtpPacketSizeForIpv4UdpOnEthernet; + case IPAddress::Version::kV6: + return kMaxRtpPacketSizeForIpv6UdpOnEthernet; + default: + OSP_NOTREACHED(); + return 0; + } +} + +void Environment::SendPacket(absl::Span<const uint8_t> packet) { + OSP_DCHECK(remote_endpoint_.address); + OSP_DCHECK_NE(remote_endpoint_.port, 0); + if (socket_) { + socket_->SendMessage(packet.data(), packet.size(), remote_endpoint_); + } +} + +Environment::PacketConsumer::~PacketConsumer() = default; + +void Environment::OnError(UdpSocket* socket, Error error) { + // Usually OnError() is only called for non-recoverable Errors. However, + // OnSendError() and OnRead() delegate to this method, to handle their hard + // error cases as well. So, return early here if |error| is recoverable. + if (error.ok() || error.code() == Error::Code::kAgain) { + return; + } + + if (socket_error_handler_) { + socket_error_handler_(error); + return; + } + + // Default behavior when no error handler is set. + OSP_LOG_ERROR << "For UDP socket bound to " << socket_->GetLocalEndpoint() + << ": " << error; +} + +void Environment::OnSendError(UdpSocket* socket, Error error) { + OnError(socket, error); +} + +void Environment::OnRead(UdpSocket* socket, + ErrorOr<UdpPacket> packet_or_error) { + if (!packet_consumer_) { + return; + } + + if (packet_or_error.is_error()) { + OnError(socket, packet_or_error.error()); + return; + } + + // Ideally, the arrival time would come from the operating system's network + // stack (e.g., by using the SO_TIMESTAMP sockopt on POSIX systems). However, + // there would still be the problem of mapping the timestamp to a value in + // terms of platform::Clock. So, just sample the Clock here and call that the + // "arrival time." While this can add variance within the system, it should be + // minimal, assuming not too much time has elapsed between the actual packet + // receive event and the when this code here is executing. + const Clock::time_point arrival_time = now_function_(); + + UdpPacket packet = std::move(packet_or_error.value()); + packet_consumer_->OnReceivedPacket( + packet.source(), arrival_time, + std::move(static_cast<std::vector<uint8_t>&>(packet))); +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/environment.h b/cast/streaming/environment.h new file mode 100644 index 00000000..0bc99f2d --- /dev/null +++ b/cast/streaming/environment.h @@ -0,0 +1,131 @@ +// 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. + +#ifndef CAST_STREAMING_ENVIRONMENT_H_ +#define CAST_STREAMING_ENVIRONMENT_H_ + +#include <stdint.h> + +#include <functional> +#include <memory> + +#include "absl/types/span.h" +#include "platform/api/time.h" +#include "platform/api/udp_socket.h" +#include "platform/base/ip_address.h" + +namespace openscreen { +namespace platform { +class TaskRunner; +} // namespace platform +} // namespace openscreen + +namespace cast { +namespace streaming { + +// Provides the common environment for operating system resources shared by +// multiple components. +class Environment : public openscreen::platform::UdpSocket::Client { + public: + class PacketConsumer { + public: + virtual void OnReceivedPacket( + const openscreen::IPEndpoint& source, + openscreen::platform::Clock::time_point arrival_time, + std::vector<uint8_t> packet) = 0; + + protected: + virtual ~PacketConsumer(); + }; + + // Construct with the given clock source and TaskRunner. Creates and + // internally-owns a UdpSocket, and immediately binds it to the given + // |local_endpoint|. + Environment(openscreen::platform::ClockNowFunctionPtr now_function, + openscreen::platform::TaskRunner* task_runner, + const openscreen::IPEndpoint& local_endpoint); + + ~Environment() override; + + openscreen::platform::ClockNowFunctionPtr now_function() const { + return now_function_; + } + openscreen::platform::TaskRunner* task_runner() const { return task_runner_; } + + // Returns the local endpoint the socket is bound to, or the zero IPEndpoint + // if socket creation/binding failed. + openscreen::IPEndpoint GetBoundLocalEndpoint() const; + + // Set a handler function to run whenever non-recoverable socket errors occur. + // If never set, the default is to emit log messages at error priority. + void set_socket_error_handler( + std::function<void(openscreen::Error)> handler) { + socket_error_handler_ = handler; + } + + // Get/Set the remote endpoint. This is separate from the constructor because + // the remote endpoint is, in some cases, discovered only after receiving a + // packet. + const openscreen::IPEndpoint& remote_endpoint() const { + return remote_endpoint_; + } + void set_remote_endpoint(const openscreen::IPEndpoint& endpoint) { + remote_endpoint_ = endpoint; + } + + // Start/Resume delivery of incoming packets to the given |packet_consumer|. + // Delivery will continue until DropIncomingPackets() is called. + void ConsumeIncomingPackets(PacketConsumer* packet_consumer); + + // Stop delivery of incoming packets, dropping any that do come in. All + // internal references to the PacketConsumer that was provided in the last + // call to ConsumeIncomingPackets() are cleared. + void DropIncomingPackets(); + + // Returns the maximum packet size for the network. This will always return a + // value of at least kRequiredNetworkPacketSize. + int GetMaxPacketSize() const; + + // Sends the given |packet| to the remote endpoint, best-effort. + // set_remote_endpoint() must be called beforehand with a valid IPEndpoint. + // + // Note: This method is virtual to allow unit tests to intercept packets + // before they actually head-out through the socket. + virtual void SendPacket(absl::Span<const uint8_t> packet); + + protected: + // Common constructor that just stores the injected dependencies and does not + // create a socket. Subclasses use this to provide an alternative packet + // receive/send mechanism (e.g., for testing). + Environment(openscreen::platform::ClockNowFunctionPtr now_function, + openscreen::platform::TaskRunner* task_runner); + + private: + // openscreen::platform::UdpSocket::Client implementation. + void OnError(openscreen::platform::UdpSocket* socket, + openscreen::Error error) final; + void OnSendError(openscreen::platform::UdpSocket* socket, + openscreen::Error error) final; + void OnRead(openscreen::platform::UdpSocket* socket, + openscreen::ErrorOr<openscreen::platform::UdpPacket> + packet_or_error) final; + + const openscreen::platform::ClockNowFunctionPtr now_function_; + openscreen::platform::TaskRunner* const task_runner_; + + // The UDP socket bound to the local endpoint that was passed into the + // constructor, or null if socket creation failed. + const std::unique_ptr<openscreen::platform::UdpSocket> socket_; + + // These are externally set/cleared. Behaviors are described in getter/setter + // method comments above. + std::function<void(openscreen::Error)> socket_error_handler_; + openscreen::IPEndpoint remote_endpoint_{}; + PacketConsumer* packet_consumer_ = nullptr; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_ENVIRONMENT_H_ diff --git a/cast/streaming/expanded_value_base.h b/cast/streaming/expanded_value_base.h new file mode 100644 index 00000000..0ac0dbeb --- /dev/null +++ b/cast/streaming/expanded_value_base.h @@ -0,0 +1,164 @@ +// Copyright 2015 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. + +#ifndef CAST_STREAMING_EXPANDED_VALUE_BASE_H_ +#define CAST_STREAMING_EXPANDED_VALUE_BASE_H_ + +#include <stdint.h> + +#include <limits> + +#include "util/logging.h" + +namespace cast { +namespace streaming { + +// Abstract base template class for common "sequence value" data types such as +// RtpTimeTicks, FrameId, or PacketId which generally increment/decrement in +// predictable amounts as media is streamed, and which often need to be reliably +// truncated and re-expanded for over-the-wire transmission. +// +// FullWidthInteger should be a signed integer POD type that is of sufficiently +// high width (in bits) such that it is never expected to under/overflow during +// the longest reasonable length of continuous system operation. Subclass is +// the class inheriting the common functionality provided in this template, and +// is used to provide operator overloads. The Subclass must friend this class +// to enable these operator overloads. +// +// Please see RtpTimeTicks and unit test code for examples of how to define +// Subclasses and add features specific to their concrete data type, and how to +// use data types derived from ExpandedValueBase. For example, a RtpTimeTicks +// adds math operators consisting of the meaningful and valid set of operations +// allowed for doing "time math." On the other hand, FrameId only adds math +// operators for incrementing/decrementing since multiplication and division are +// meaningless. +template <typename FullWidthInteger, class Subclass> +class ExpandedValueBase { + static_assert(std::numeric_limits<FullWidthInteger>::is_signed, + "FullWidthInteger must be a signed integer."); + static_assert(std::numeric_limits<FullWidthInteger>::is_integer, + "FullWidthInteger must be a signed integer."); + + public: + // Methods that return the lower bits of this value. This should only be used + // for serializing/wire-formatting, and not to subvert the restricted set of + // operators allowed on this data type. + constexpr uint8_t lower_8_bits() const { + return static_cast<uint8_t>(value_); + } + constexpr uint16_t lower_16_bits() const { + return static_cast<uint16_t>(value_); + } + constexpr uint32_t lower_32_bits() const { + return static_cast<uint32_t>(value_); + } + + // Compute the greatest value less than or equal to |this| value whose lower + // bits are those of |x|. The purpose of this method is to re-instantiate an + // original value from its truncated form, usually when deserializing + // off-the-wire, when |this| value is known to be the greatest possible valid + // value. + // + // Use case example: Start with an original 32-bit value of 0x000001fe (510 + // decimal) and truncate, throwing away its upper 24 bits: 0xfe. Now, send + // this truncated value over-the-wire to a peer who needs to expand it back to + // the original 32-bit value. The peer knows that the greatest possible valid + // value is 0x00000202 (514 decimal). This method will initially attempt to + // just concatenate the upper 24 bits of |this->value_| with |x| (the 8-bit + // value), and get a result of 0x000002fe (766 decimal). However, this is + // greater than |this->value_|, so the upper 24 bits are subtracted by one to + // get 0x000001fe, which is the original value. + template <typename ShortUnsigned> + Subclass ExpandLessThanOrEqual(ShortUnsigned x) const { + static_assert(!std::numeric_limits<ShortUnsigned>::is_signed, + "|x| must be an unsigned integer."); + static_assert(std::numeric_limits<ShortUnsigned>::is_integer, + "|x| must be an unsigned integer."); + static_assert(sizeof(ShortUnsigned) <= sizeof(FullWidthInteger), + "|x| must fit within the FullWidthInteger."); + + if (sizeof(ShortUnsigned) < sizeof(FullWidthInteger)) { + // Initially, the |result| is composed of upper bits from |value_| and + // lower bits from |x|. + const FullWidthInteger short_max = + std::numeric_limits<ShortUnsigned>::max(); + FullWidthInteger result = (value_ & ~short_max) | x; + + // If the |result| is larger than |value_|, decrement the upper bits by + // one. In other words, |x| must always be interpreted as a truncated + // version of a value less than or equal to |value_|. + if (result > value_) + result -= short_max + 1; + + return Subclass(result); + } else { + // Debug builds: Ensure the highest bit is not set (which would cause + // overflow when casting to the signed integer). + OSP_DCHECK_EQ( + static_cast<ShortUnsigned>(0), + x & (static_cast<ShortUnsigned>(1) << ((sizeof(x) * 8) - 1))); + return Subclass(x); + } + } + + // Compute the smallest value greater than |this| value whose lower bits are + // those of |x|. + template <typename ShortUnsigned> + Subclass ExpandGreaterThan(ShortUnsigned x) const { + const Subclass maximum_possible_result( + value_ + std::numeric_limits<ShortUnsigned>::max() + 1); + return maximum_possible_result.ExpandLessThanOrEqual(x); + } + + // Compute the value closest to |this| value whose lower bits are those of + // |x|. The result is always within |max_distance_for_expansion()| of |this| + // value. The purpose of this method is to re-instantiate an original value + // from its truncated form, usually when deserializing off-the-wire. See + // comments for ExpandLessThanOrEqual() above for further explanation. + template <typename ShortUnsigned> + Subclass Expand(ShortUnsigned x) const { + const Subclass maximum_possible_result( + value_ + max_distance_for_expansion<ShortUnsigned>()); + return maximum_possible_result.ExpandLessThanOrEqual(x); + } + + // Comparison operators. + constexpr bool operator==(Subclass rhs) const { return value_ == rhs.value_; } + constexpr bool operator!=(Subclass rhs) const { return value_ != rhs.value_; } + constexpr bool operator<(Subclass rhs) const { return value_ < rhs.value_; } + constexpr bool operator>(Subclass rhs) const { return value_ > rhs.value_; } + constexpr bool operator<=(Subclass rhs) const { return value_ <= rhs.value_; } + constexpr bool operator>=(Subclass rhs) const { return value_ >= rhs.value_; } + + // (De)Serialize for transmission over IPC. Do not use these to subvert the + // valid set of operators allowed by this class or its Subclass. + uint64_t SerializeForIPC() const { + static_assert(sizeof(uint64_t) >= sizeof(FullWidthInteger), + "Cannot serialize FullWidthInteger into an uint64_t."); + return static_cast<uint64_t>(value_); + } + static Subclass DeserializeForIPC(uint64_t serialized) { + return Subclass(static_cast<FullWidthInteger>(serialized)); + } + + // Design limit: Values that are truncated to the ShortUnsigned type must be + // no more than this maximum distance from each other in order to ensure the + // original value can be determined correctly. + template <typename ShortUnsigned> + static constexpr FullWidthInteger max_distance_for_expansion() { + return std::numeric_limits<ShortUnsigned>::max() / 2; + } + + protected: + // Only subclasses are permitted to instantiate directly. + constexpr explicit ExpandedValueBase(FullWidthInteger value) + : value_(value) {} + + FullWidthInteger value_; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_EXPANDED_VALUE_BASE_H_ diff --git a/cast/streaming/expanded_value_base_unittest.cc b/cast/streaming/expanded_value_base_unittest.cc new file mode 100644 index 00000000..220af221 --- /dev/null +++ b/cast/streaming/expanded_value_base_unittest.cc @@ -0,0 +1,108 @@ +// Copyright 2015 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/expanded_value_base.h" + +#include "gtest/gtest.h" + +namespace cast { +namespace streaming { + +namespace { + +// A basic subclass of ExpandedValueBase to use for testing. +class TestValue : public ExpandedValueBase<int64_t, TestValue> { + public: + explicit TestValue(int64_t value) : ExpandedValueBase(value) {} +}; + +} // namespace + +// Tests the various scenarios of truncating and then re-expanding values, and +// confirming the correct behavior. Note that, while the code below just tests +// truncation/expansion to/from 8 bits, the 16- and 32-bit cases are implicitly +// confirmed because the Expand() method uses the compiler to derive all of its +// constants (based on the type of its argument). +TEST(ExpandedValueBaseTest, TruncationAndExpansion) { + // Test that expansion works when the reference is always equal to the value + // that was truncated. + for (int64_t i = -512; i <= 512; ++i) { + const TestValue original_value(i); + const uint8_t truncated = original_value.lower_8_bits(); + const TestValue reference(i); + ASSERT_EQ(original_value, reference.Expand(truncated)) << "i=" << i; + } + + // Test that expansion works when the reference is always one less than the + // value that was truncated. + for (int64_t i = -512; i <= 512; ++i) { + const TestValue original_value(i); + const uint8_t truncated = original_value.lower_8_bits(); + const TestValue reference(i - 1); + ASSERT_EQ(original_value, reference.Expand(truncated)) << "i=" << i; + } + + // Test that expansion works when the reference is always one greater than the + // value that was truncated. + for (int64_t i = -512; i <= 512; ++i) { + const TestValue original_value(i); + const uint8_t truncated = original_value.lower_8_bits(); + const TestValue reference(i + 1); + ASSERT_EQ(original_value, reference.Expand(truncated)) << "i=" << i; + } + + // Test cases where the difference between the original value and the fixed + // reference is within the range [-128,+127]. The truncated value should + // always be re-expanded to the original value. + for (int64_t bias = -5; bias <= 5; ++bias) { + for (int64_t i = -128; i <= 127; ++i) { + const TestValue original_value(bias + i); + const uint8_t truncated = original_value.lower_8_bits(); + const TestValue reference(bias); + ASSERT_EQ(original_value, reference.Expand(truncated)) + << "bias=" << bias << ", i=" << i; + } + } + + // Test cases where the difference between the original value and the fixed + // reference is within the range [+128,+255]. When the truncated value is + // re-expanded, it should be 256 less than the original value. + for (int64_t bias = -5; bias <= 5; ++bias) { + for (int64_t i = 128; i <= 255; ++i) { + // Example: Let |original_value| be 192. Then, the truncated 8-bit value + // will be 0xc0. When a |reference| of zero is asked to expand 0xc0 back + // to the original value, it should produce -64 since -64 is closer to + // |reference| than 192. + const TestValue original_value(bias + i); + const uint8_t truncated = original_value.lower_8_bits(); + const TestValue reexpanded_value(bias + i - 256); + ASSERT_EQ(reexpanded_value.lower_8_bits(), truncated); + const TestValue reference(bias); + ASSERT_EQ(reexpanded_value, reference.Expand(truncated)) + << "bias=" << bias << ", i=" << i; + } + } + + // Test cases where the difference between the original value and the fixed + // reference is within the range [-256,-129]. When the truncated value is + // re-expanded, it should be 256 more than the original value. + for (int64_t bias = -5; bias <= 5; ++bias) { + for (int64_t i = -256; i <= -129; ++i) { + // Example: Let |original_value| be -192. Then, the truncated 8-bit value + // will be 0x40. When a |reference| of zero is asked to expand 0x40 back + // to the original value, it should produce 64 since 64 is closer to the + // |reference| than -192. + const TestValue original_value(bias + i); + const uint8_t truncated = original_value.lower_8_bits(); + const TestValue reexpanded_value(bias + i + 256); + ASSERT_EQ(reexpanded_value.lower_8_bits(), truncated); + const TestValue reference(bias); + ASSERT_EQ(reexpanded_value, reference.Expand(truncated)) + << "bias=" << bias << ", i=" << i; + } + } +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/frame_collector.cc b/cast/streaming/frame_collector.cc new file mode 100644 index 00000000..aabbdd06 --- /dev/null +++ b/cast/streaming/frame_collector.cc @@ -0,0 +1,160 @@ +// 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/frame_collector.h" + +#include <algorithm> +#include <limits> +#include <numeric> + +#include "cast/streaming/frame_id.h" +#include "cast/streaming/rtp_defines.h" +#include "util/logging.h" + +namespace cast { +namespace streaming { + +namespace { + +// Integer constant representing that the number of packets is not yet known. +constexpr int kUnknownNumberOfPackets = std::numeric_limits<int>::max(); + +} // namespace + +FrameCollector::FrameCollector() + : num_missing_packets_(kUnknownNumberOfPackets) {} + +FrameCollector::~FrameCollector() = default; + +bool FrameCollector::CollectRtpPacket(const RtpPacketParser::ParseResult& part, + std::vector<uint8_t>* buffer) { + OSP_DCHECK(!frame_.frame_id.is_null()); + + if (part.frame_id != frame_.frame_id) { + OSP_LOG_WARN + << "Ignoring potentially corrupt packet (frame ID mismatch). Expected: " + << frame_.frame_id << " Got: " << part.frame_id; + return false; + } + + const int frame_packet_count = static_cast<int>(part.max_packet_id) + 1; + if (num_missing_packets_ == kUnknownNumberOfPackets) { + // This is the first packet being processed for the frame. + num_missing_packets_ = frame_packet_count; + chunks_.resize(num_missing_packets_); + } else { + // Since this is not the first packet being processed, sanity-check that the + // "frame ID" and "max packet ID" are the expected values. + if (frame_packet_count != static_cast<int>(chunks_.size())) { + OSP_LOG_WARN << "Ignoring potentially corrupt packet (packet count " + "mismatch). packet_count=" + << chunks_.size() << " is not equal to 1 + max_packet_id=" + << part.max_packet_id; + return false; + } + } + + // The packet ID must not be greater than the max packet ID. + if (part.packet_id >= chunks_.size()) { + OSP_LOG_WARN + << "Ignoring potentially corrupt packet having invalid packet ID " + << part.packet_id << " (should be less than " << chunks_.size() << ")."; + return false; + } + + // Don't process duplicate packets. + if (chunks_[part.packet_id].has_data()) { + // Note: No logging here because this is a common occurrence that is not + // indicative of any problem in the system. + return true; + } + + // Populate metadata from packet 0 only, which is the only packet that must + // contain a complete set of values. + if (part.packet_id == FramePacketId{0}) { + if (part.is_key_frame) { + frame_.dependency = EncodedFrame::KEY_FRAME; + } else if (part.frame_id == part.referenced_frame_id) { + frame_.dependency = EncodedFrame::INDEPENDENTLY_DECODABLE; + } else { + frame_.dependency = EncodedFrame::DEPENDS_ON_ANOTHER; + } + frame_.referenced_frame_id = part.referenced_frame_id; + frame_.rtp_timestamp = part.rtp_timestamp; + frame_.new_playout_delay = part.new_playout_delay; + } + + // Take ownership of the contents of the |buffer| (no copy!), and record the + // region of the buffer containing the payload data. The payload region is + // usually all but the first few dozen bytes of the buffer. + PayloadChunk& chunk = chunks_[part.packet_id]; + chunk.buffer.swap(*buffer); + chunk.payload = part.payload; + OSP_DCHECK_GE(chunk.payload.data(), chunk.buffer.data()); + OSP_DCHECK_LE(chunk.payload.data() + chunk.payload.size(), + chunk.buffer.data() + chunk.buffer.size()); + + // Success! + --num_missing_packets_; + OSP_DCHECK_GE(num_missing_packets_, 0); + return true; +} + +void FrameCollector::GetMissingPackets(std::vector<PacketNack>* nacks) const { + OSP_DCHECK(!frame_.frame_id.is_null()); + + if (num_missing_packets_ == 0) { + return; + } + + const int frame_packet_count = chunks_.size(); + if (num_missing_packets_ >= frame_packet_count) { + nacks->push_back(PacketNack{frame_.frame_id, kAllPacketsLost}); + return; + } + + for (int packet_id = 0; packet_id < frame_packet_count; ++packet_id) { + if (!chunks_[packet_id].has_data()) { + nacks->push_back( + PacketNack{frame_.frame_id, static_cast<FramePacketId>(packet_id)}); + } + } +} + +const EncryptedFrame& FrameCollector::PeekAtAssembledFrame() { + OSP_DCHECK_EQ(num_missing_packets_, 0); + + if (!frame_.data.data()) { + // Allocate the frame's payload buffer once, right-sized to the sum of all + // chunk sizes. + frame_.owned_data_.reserve( + std::accumulate(chunks_.cbegin(), chunks_.cend(), size_t{0}, + [](size_t num_bytes_so_far, const PayloadChunk& chunk) { + return num_bytes_so_far + chunk.payload.size(); + })); + // Now, populate the frame's payload buffer with each chunk of data. + for (const PayloadChunk& chunk : chunks_) { + frame_.owned_data_.insert(frame_.owned_data_.end(), chunk.payload.begin(), + chunk.payload.end()); + } + frame_.data = absl::Span<uint8_t>(frame_.owned_data_); + } + + return frame_; +} + +void FrameCollector::Reset() { + num_missing_packets_ = kUnknownNumberOfPackets; + frame_.frame_id = FrameId(); + frame_.owned_data_.clear(); + frame_.owned_data_.shrink_to_fit(); + frame_.data = absl::Span<uint8_t>(); + chunks_.clear(); +} + +FrameCollector::PayloadChunk::PayloadChunk() = default; +FrameCollector::PayloadChunk::~PayloadChunk() = default; + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/frame_collector.h b/cast/streaming/frame_collector.h new file mode 100644 index 00000000..7504d690 --- /dev/null +++ b/cast/streaming/frame_collector.h @@ -0,0 +1,90 @@ +// 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. + +#ifndef CAST_STREAMING_FRAME_COLLECTOR_H_ +#define CAST_STREAMING_FRAME_COLLECTOR_H_ + +#include <vector> + +#include "absl/types/span.h" +#include "cast/streaming/frame_crypto.h" +#include "cast/streaming/frame_id.h" +#include "cast/streaming/rtcp_common.h" +#include "cast/streaming/rtp_packet_parser.h" + +namespace cast { +namespace streaming { + +// Used by a Receiver to collect the parts of a frame, track what is +// missing/complete, and assemble a complete frame. +class FrameCollector { + public: + FrameCollector(); + ~FrameCollector(); + + // Sets the ID of the current frame being collected. This must be called after + // each Reset(), and before any of the other methods. + void set_frame_id(FrameId frame_id) { frame_.frame_id = frame_id; } + + // Examine the parsed packet, representing part of the whole frame, and + // collect any data/metadata from it that helps complete the frame. Returns + // false if the |part| contained invalid data. On success, this method takes + // the data contained within the |buffer|, into which |part.payload| is + // pointing, in lieu of copying the data. + [[nodiscard]] bool CollectRtpPacket(const RtpPacketParser::ParseResult& part, + std::vector<uint8_t>* buffer); + + // Returns true if the frame data collection is complete and the frame can be + // assembled. + bool is_complete() const { return num_missing_packets_ == 0; } + + // Appends zero or more elements to |nacks| representing which packets are not + // yet collected. If all packets for the frame are missing, this appends a + // single element containing the special kAllPacketsLost packet ID. Otherwise, + // one element is appended for each missing packet, in increasing order of + // packet ID. + void GetMissingPackets(std::vector<PacketNack>* nacks) const; + + // Returns a read-only reference to the completely-collected frame, assembling + // it if necessary. The caller should reset the FrameCollector (see Reset() + // below) to free-up memory once it has finished reading from the returned + // frame. + // + // Precondition: is_complete() must return true before this method can be + // called. + const EncryptedFrame& PeekAtAssembledFrame(); + + // Resets the FrameCollector back to its initial state, freeing-up memory. + void Reset(); + + private: + struct PayloadChunk { + std::vector<uint8_t> buffer; + absl::Span<const uint8_t> payload; // Once set, is within |buffer.data()|. + + PayloadChunk(); + ~PayloadChunk(); + + bool has_data() const { return !!payload.data(); } + }; + + // Storage for frame metadata and data. Once the frame has been completely + // collected and assembled, |frame_.data| is set to non-null, and this is + // exposed externally (read-only). + EncryptedFrame frame_; + + // The number of packets needed to complete the frame, or the maximum int if + // this is not yet known. + int num_missing_packets_; + + // The chunks of payload data being collected, where element indices + // correspond 1:1 with packet IDs. When the first part is collected, this is + // resized to match the total number of packets being expected. + std::vector<PayloadChunk> chunks_; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_FRAME_COLLECTOR_H_ diff --git a/cast/streaming/frame_collector_unittest.cc b/cast/streaming/frame_collector_unittest.cc new file mode 100644 index 00000000..53e13811 --- /dev/null +++ b/cast/streaming/frame_collector_unittest.cc @@ -0,0 +1,211 @@ +// 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/frame_collector.h" + +#include <stdint.h> + +#include <algorithm> +#include <vector> + +#include "cast/streaming/encoded_frame.h" +#include "cast/streaming/frame_id.h" +#include "cast/streaming/rtcp_common.h" +#include "cast/streaming/rtp_time.h" +#include "gtest/gtest.h" + +namespace cast { +namespace streaming { +namespace { + +const FrameId kSomeFrameId = FrameId::first() + 39; +constexpr RtpTimeTicks kSomeRtpTimestamp = + RtpTimeTicks() + RtpTimeDelta::FromTicks(90000); + +// Convenience macro to check that the |collector| generates an expected set of +// NACKs. +#define EXPECT_HAS_NACKS(expected_nacks, collector) \ + do { \ + std::vector<PacketNack> nacks; \ + (collector).GetMissingPackets(&nacks); \ + EXPECT_EQ((expected_nacks), nacks); \ + } while (false); + +TEST(FrameCollectorTest, CollectsFrameWithOnlyOnePart) { + FrameCollector collector; + + // Run for two frames to test that the collector can be re-used. + for (int i = 0; i < 2; ++i) { + const FrameId frame_id = kSomeFrameId + i; + collector.set_frame_id(frame_id); + EXPECT_FALSE(collector.is_complete()); + + // With no packets seen yet, the collector should provide the "all packets + // lost" NACK. + EXPECT_HAS_NACKS((std::vector<PacketNack>{{frame_id, kAllPacketsLost}}), + collector); + + // Collect the single packet of the frame. + RtpPacketParser::ParseResult part; + part.rtp_timestamp = kSomeRtpTimestamp + (RtpTimeDelta::FromTicks(200) * i); + part.frame_id = frame_id; + part.packet_id = 0; + part.max_packet_id = 0; + if (i == 0) { + part.is_key_frame = true; + // Do not set |part.new_playout_delay|. + } else { + part.is_key_frame = false; + part.new_playout_delay = std::chrono::milliseconds(800); + } + part.referenced_frame_id = kSomeFrameId; + std::vector<uint8_t> buffer(255); + for (int j = 0; j < 255; ++j) { + buffer[j] = static_cast<uint8_t>(j); + } + part.payload = absl::Span<uint8_t>(buffer); + EXPECT_TRUE(collector.CollectRtpPacket(part, &buffer)); + + // At this point, the collector should feel complete. + EXPECT_TRUE(collector.is_complete()); + EXPECT_HAS_NACKS(std::vector<PacketNack>(), collector); + + // Examine the assembled frame, and confirm its metadata and payload match + // what was put into the collector via the packet above. + const auto& frame = collector.PeekAtAssembledFrame(); + if (i == 0) { + EXPECT_EQ(EncodedFrame::KEY_FRAME, frame.dependency); + EXPECT_EQ(std::chrono::milliseconds(), frame.new_playout_delay); + } else { + EXPECT_EQ(EncodedFrame::DEPENDS_ON_ANOTHER, frame.dependency); + EXPECT_EQ(std::chrono::milliseconds(800), frame.new_playout_delay); + } + EXPECT_EQ(part.frame_id, frame.frame_id); + EXPECT_EQ(kSomeFrameId, frame.referenced_frame_id); + EXPECT_EQ(part.rtp_timestamp, frame.rtp_timestamp); + for (int j = 0; j < 255; ++j) { + EXPECT_EQ(static_cast<uint8_t>(j), frame.data[j]); + } + + collector.Reset(); + } +} + +TEST(FrameCollectorTest, CollectsFrameWithMultiplePartsArrivingOutOfOrder) { + FrameCollector collector; + collector.set_frame_id(kSomeFrameId); + + // With no packets seen yet, the collector should provide the "all packets + // lost" NACK. + EXPECT_HAS_NACKS((std::vector<PacketNack>{{kSomeFrameId, kAllPacketsLost}}), + collector); + + // Prepare the six packet payloads, and the list of remaining NACKs (checked + // after each part is collected). + const int kPayloadSizes[] = {999, 998, 998, 998, 42, 0}; + std::vector<uint8_t> payloads[6]; + std::vector<PacketNack> remaining_nacks; + for (int i = 0; i < 6; ++i) { + payloads[i].resize(kPayloadSizes[i], static_cast<uint8_t>(i)); + remaining_nacks.push_back( + PacketNack{kSomeFrameId, static_cast<FramePacketId>(i)}); + } + + // Collect all six packets, out-of-order, and with some duplicates. + constexpr FramePacketId kPacketIds[] = {2, 0, 1, 2, 4, 3, 5, 5, 5, 0}; + for (FramePacketId packet_id : kPacketIds) { + RtpPacketParser::ParseResult part{}; + part.rtp_timestamp = kSomeRtpTimestamp; + part.is_key_frame = true; + part.frame_id = kSomeFrameId; + part.packet_id = packet_id; + part.max_packet_id = 5; + part.referenced_frame_id = kSomeFrameId; + // Prepare a copy of the payload to pass into the FrameCollector. Place 24 + // bytes of bogus data at the start of the buffer to simulate the + // non-payload part of the RTP packet. + std::vector<uint8_t> buffer(24, uint8_t{0xab}); + buffer.insert(buffer.end(), payloads[packet_id].begin(), + payloads[packet_id].end()); + part.payload = absl::Span<uint8_t>(buffer.data() + 24, buffer.size() - 24); + EXPECT_TRUE(collector.CollectRtpPacket(part, &buffer)); + + // Remove the packet from the list of expected remaining NACKs, and then + // check that the collector agrees. + remaining_nacks.erase( + std::remove_if(remaining_nacks.begin(), remaining_nacks.end(), + [packet_id](const PacketNack& nack) { + return nack.packet_id == packet_id; + }), + remaining_nacks.end()); + EXPECT_HAS_NACKS(remaining_nacks, collector); + } + + // Confirm there are no missing packets and no NACKs generated. + EXPECT_TRUE(collector.is_complete()); + EXPECT_HAS_NACKS(std::vector<PacketNack>(), collector); + + // Examine the assembled frame, and confirm its metadata and payload match + // what was put into the collector via the packets above, and that the payload + // bytes are in-order. + const auto& frame = collector.PeekAtAssembledFrame(); + EXPECT_EQ(EncodedFrame::KEY_FRAME, frame.dependency); + EXPECT_EQ(kSomeFrameId, frame.frame_id); + EXPECT_EQ(kSomeFrameId, frame.referenced_frame_id); + EXPECT_EQ(kSomeRtpTimestamp, frame.rtp_timestamp); + absl::Span<const uint8_t> remaining_data = frame.data; + for (int i = 0; i < 6; ++i) { + ASSERT_LE(kPayloadSizes[i], static_cast<int>(remaining_data.size())); + EXPECT_EQ(absl::Span<const uint8_t>(payloads[i]), + remaining_data.subspan(0, kPayloadSizes[i])) + << "i=" << i; + remaining_data.remove_prefix(kPayloadSizes[i]); + } + ASSERT_TRUE(remaining_data.empty()); +} + +TEST(FrameCollectorTest, RejectsInvalidParts) { + FrameCollector collector; + + // Expect the collector rejects a part not having the correct FrameId. + collector.set_frame_id(kSomeFrameId + 256); + RtpPacketParser::ParseResult part{}; + part.rtp_timestamp = kSomeRtpTimestamp; + part.is_key_frame = false; + part.frame_id = kSomeFrameId; + part.packet_id = 0; + part.max_packet_id = 3; + std::vector<uint8_t> buffer(1, 'A'); + part.payload = absl::Span<uint8_t>(buffer); + EXPECT_FALSE(collector.CollectRtpPacket(part, &buffer)); + // Note: When CollectRtpPacket() returns false, it does not take ownership of + // the buffer memory. + ASSERT_TRUE(buffer.size() == 1 && buffer[0] == 'A'); + + // The collector should accept a part having the correct FrameId. + collector.set_frame_id(kSomeFrameId); + part.frame_id = kSomeFrameId; + EXPECT_TRUE(collector.CollectRtpPacket(part, &buffer)); + // Note: Re-assign the buffer and payload pointer since the buffer was just + // consumed. + buffer.assign(1, 'A'); + part.payload = absl::Span<uint8_t>(buffer); + + // The collector should reject a part where the packet_id is greater than the + // previously-established max_packet_id. + part.packet_id = 5; // BAD, since max_packet_id is 3 (see above). + EXPECT_FALSE(collector.CollectRtpPacket(part, &buffer)); + ASSERT_TRUE(buffer.size() == 1 && buffer[0] == 'A'); + + // The collector should reject a part where the max_packet_id disagrees with + // previously-established max_packet_id. + part.packet_id = 2; + part.max_packet_id = 5; // BAD, since max_packet_id is 3 (see above). + EXPECT_FALSE(collector.CollectRtpPacket(part, &buffer)); + ASSERT_TRUE(buffer.size() == 1 && buffer[0] == 'A'); +} + +} // namespace +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/frame_crypto.cc b/cast/streaming/frame_crypto.cc new file mode 100644 index 00000000..416c2df1 --- /dev/null +++ b/cast/streaming/frame_crypto.cc @@ -0,0 +1,120 @@ +// 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/frame_crypto.h" + +#include <random> + +#include "openssl/crypto.h" +#include "openssl/err.h" +#include "openssl/rand.h" +#include "util/big_endian.h" +#include "util/crypto/openssl_util.h" + +namespace cast { +namespace streaming { + +EncryptedFrame::EncryptedFrame() { + data = absl::Span<uint8_t>(owned_data_); +} + +EncryptedFrame::~EncryptedFrame() = default; + +EncryptedFrame::EncryptedFrame(EncryptedFrame&& other) MAYBE_NOEXCEPT + : EncodedFrame(static_cast<EncodedFrame&&>(other)), + owned_data_(std::move(other.owned_data_)) { + data = absl::Span<uint8_t>(owned_data_); + other.data = absl::Span<uint8_t>{}; +} + +EncryptedFrame& EncryptedFrame::operator=(EncryptedFrame&& other) + MAYBE_NOEXCEPT { + this->EncodedFrame::operator=(static_cast<EncodedFrame&&>(other)); + owned_data_ = std::move(other.owned_data_); + data = absl::Span<uint8_t>(owned_data_); + other.data = absl::Span<uint8_t>{}; + return *this; +} + +FrameCrypto::FrameCrypto(const std::array<uint8_t, 16>& aes_key, + const std::array<uint8_t, 16>& cast_iv_mask) + : aes_key_{}, cast_iv_mask_(cast_iv_mask) { + // Ensure that the library has been initialized. CRYPTO_library_init() may be + // safely called multiple times during the life of a process. + CRYPTO_library_init(); + + // Initialize the 244-byte AES_KEY struct once, here at construction time. The + // const_cast<> is reasonable as this is a one-time-ctor-initialized value + // that will remain constant from here onward. + const int return_code = AES_set_encrypt_key( + aes_key.data(), aes_key.size() * 8, const_cast<AES_KEY*>(&aes_key_)); + if (return_code != 0) { + ClearOpenSSLERRStack(CURRENT_LOCATION); + OSP_LOG_FATAL << "Failure when setting encryption key; unsafe to continue."; + OSP_NOTREACHED(); + } +} + +FrameCrypto::~FrameCrypto() = default; + +EncryptedFrame FrameCrypto::Encrypt(const EncodedFrame& encoded_frame) const { + EncryptedFrame result; + encoded_frame.CopyMetadataTo(&result); + result.owned_data_.resize(encoded_frame.data.size()); + result.data = absl::Span<uint8_t>(result.owned_data_); + EncryptCommon(encoded_frame.frame_id, encoded_frame.data, result.data); + return result; +} + +void FrameCrypto::Decrypt(const EncryptedFrame& encrypted_frame, + EncodedFrame* encoded_frame) const { + encrypted_frame.CopyMetadataTo(encoded_frame); + // AES-CTC is symmetric. Thus, decryption back to the plaintext is the same as + // encrypting the ciphertext; and both are the same size. + if (encrypted_frame.data.size() < encoded_frame->data.size()) { + encoded_frame->data = absl::Span<uint8_t>(encoded_frame->data.data(), + encrypted_frame.data.size()); + } + EncryptCommon(encrypted_frame.frame_id, encrypted_frame.data, + encoded_frame->data); +} + +void FrameCrypto::EncryptCommon(FrameId frame_id, + absl::Span<const uint8_t> in, + absl::Span<uint8_t> out) const { + OSP_DCHECK(!frame_id.is_null()); + OSP_DCHECK_EQ(in.size(), out.size()); + + // Compute the AES nonce for Cast Streaming payload encryption, which is based + // on the |frame_id|. + std::array<uint8_t, 16> aes_nonce{/* zero initialized */}; + static_assert(AES_BLOCK_SIZE == sizeof(aes_nonce), + "AES_BLOCK_SIZE is not 16 bytes."); + openscreen::WriteBigEndian<uint32_t>(frame_id.lower_32_bits(), + aes_nonce.data() + 8); + for (size_t i = 0; i < aes_nonce.size(); ++i) { + aes_nonce[i] ^= cast_iv_mask_[i]; + } + + std::array<uint8_t, 16> ecount_buf{/* zero initialized */}; + unsigned int block_offset = 0; + AES_ctr128_encrypt(in.data(), out.data(), in.size(), &aes_key_, + aes_nonce.data(), ecount_buf.data(), &block_offset); +} + +// static +std::array<uint8_t, 16> FrameCrypto::GenerateRandomBytes() { + std::array<uint8_t, 16> result; + const int return_code = RAND_bytes(result.data(), sizeof(result)); + if (return_code != 1) { + ClearOpenSSLERRStack(CURRENT_LOCATION); + OSP_LOG_FATAL + << "Failure when generating random bytes; unsafe to continue."; + OSP_NOTREACHED(); + } + return result; +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/frame_crypto.h b/cast/streaming/frame_crypto.h new file mode 100644 index 00000000..693385ad --- /dev/null +++ b/cast/streaming/frame_crypto.h @@ -0,0 +1,97 @@ +// 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. + +#ifndef CAST_STREAMING_FRAME_CRYPTO_H_ +#define CAST_STREAMING_FRAME_CRYPTO_H_ + +#include <stddef.h> +#include <stdint.h> + +#include <array> +#include <vector> + +#include "absl/types/span.h" +#include "cast/streaming/encoded_frame.h" +#include "openssl/aes.h" +#include "platform/base/macros.h" + +namespace cast { +namespace streaming { + +class FrameCollector; +class FrameCrypto; + +// A subclass of EncodedFrame that represents an EncodedFrame with encrypted +// payload data, and owns the buffer storing the encrypted payload data. Use +// FrameCrypto (below) to explicitly convert between EncryptedFrames and +// EncodedFrames. +struct EncryptedFrame : public EncodedFrame { + EncryptedFrame(); + ~EncryptedFrame(); + EncryptedFrame(EncryptedFrame&&) MAYBE_NOEXCEPT; + EncryptedFrame& operator=(EncryptedFrame&&) MAYBE_NOEXCEPT; + + protected: + // Since only FrameCrypto and FrameCollector are trusted to generate the + // payload data, only they are allowed direct access to the storage. + friend class FrameCollector; + friend class FrameCrypto; + + // Note: EncodedFrame::data must be updated whenever any mutations are + // performed on this member! + std::vector<uint8_t> owned_data_; +}; + +// Encrypts EncodedFrames before sending, or decrypts EncryptedFrames that have +// been received. +class FrameCrypto { + public: + // Construct with the given 16-bytes AES key and IV mask. Both arguments + // should be randomly-generated for each new streaming session. + // GenerateRandomBytes() can be used to create them. + FrameCrypto(const std::array<uint8_t, 16>& aes_key, + const std::array<uint8_t, 16>& cast_iv_mask); + + ~FrameCrypto(); + + EncryptedFrame Encrypt(const EncodedFrame& encoded_frame) const; + + // Decrypt the given |encrypted_frame| into the output |encoded_frame|. The + // caller must provide a sufficiently-sized data buffer (see + // GetPlaintextSize()). + void Decrypt(const EncryptedFrame& encrypted_frame, + EncodedFrame* encoded_frame) const; + + // AES crypto inputs and outputs (for either encrypting or decrypting) are + // always the same size in bytes. The following are just "documentative code." + static int GetEncryptedSize(const EncodedFrame& encoded_frame) { + return encoded_frame.data.size(); + } + static int GetPlaintextSize(const EncryptedFrame& encrypted_frame) { + return encrypted_frame.data.size(); + } + + // Returns random bytes from a cryptographically-secure RNG source. + static std::array<uint8_t, 16> GenerateRandomBytes(); + + private: + // The 244-byte AES_KEY struct, derived from the |aes_key| passed to the ctor, + // and initialized by boringssl's AES_set_encrypt_key() function. + const AES_KEY aes_key_; + + // Random bytes used in the custom heuristic to generate a different + // initialization vector for each frame. + const std::array<uint8_t, 16> cast_iv_mask_; + + // AES-CTR is symmetric. Thus, the "meat" of both Encrypt() and Decrypt() is + // the same. + void EncryptCommon(FrameId frame_id, + absl::Span<const uint8_t> in, + absl::Span<uint8_t> out) const; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_FRAME_CRYPTO_H_ diff --git a/cast/streaming/frame_crypto_unittest.cc b/cast/streaming/frame_crypto_unittest.cc new file mode 100644 index 00000000..c5fcd400 --- /dev/null +++ b/cast/streaming/frame_crypto_unittest.cc @@ -0,0 +1,80 @@ +// 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/frame_crypto.h" + +#include <array> +#include <cstring> +#include <vector> + +#include "gtest/gtest.h" + +namespace cast { +namespace streaming { +namespace { + +TEST(FrameCryptoTest, EncryptsAndDecryptsFrames) { + // Prepare two frames with different FrameIds, but having the same payload + // bytes. + EncodedFrame frame0; + frame0.frame_id = FrameId::first(); + const char kPayload[] = "The quick brown fox jumps over the lazy dog."; + std::vector<uint8_t> buffer( + reinterpret_cast<const uint8_t*>(kPayload), + reinterpret_cast<const uint8_t*>(kPayload) + sizeof(kPayload)); + frame0.data = absl::Span<uint8_t>(buffer); + EncodedFrame frame1; + frame1.frame_id = frame0.frame_id + 1; + frame1.data = frame0.data; + + const std::array<uint8_t, 16> key = FrameCrypto::GenerateRandomBytes(); + const std::array<uint8_t, 16> iv = FrameCrypto::GenerateRandomBytes(); + EXPECT_NE(0, memcmp(key.data(), iv.data(), sizeof(key))); + const FrameCrypto crypto(key, iv); + + // Encrypt both frames, and confirm the encrypted data is something other than + // the plaintext, and that both frames have different encrypted data. + const EncryptedFrame encrypted_frame0 = crypto.Encrypt(frame0); + EXPECT_EQ(frame0.frame_id, encrypted_frame0.frame_id); + ASSERT_EQ(static_cast<int>(frame0.data.size()), + FrameCrypto::GetPlaintextSize(encrypted_frame0)); + EXPECT_NE(0, memcmp(frame0.data.data(), encrypted_frame0.data.data(), + frame0.data.size())); + const EncryptedFrame encrypted_frame1 = crypto.Encrypt(frame1); + EXPECT_EQ(frame1.frame_id, encrypted_frame1.frame_id); + ASSERT_EQ(static_cast<int>(frame1.data.size()), + FrameCrypto::GetPlaintextSize(encrypted_frame1)); + EXPECT_NE(0, memcmp(frame1.data.data(), encrypted_frame1.data.data(), + frame1.data.size())); + ASSERT_EQ(encrypted_frame0.data.size(), encrypted_frame1.data.size()); + EXPECT_NE(0, + memcmp(encrypted_frame0.data.data(), encrypted_frame1.data.data(), + encrypted_frame0.data.size())); + + // Now, decrypt the encrypted frames, and confirm the original payload + // plaintext is retrieved. + EncodedFrame decrypted_frame0; + std::vector<uint8_t> decrypted_frame0_buffer( + FrameCrypto::GetPlaintextSize(encrypted_frame0)); + decrypted_frame0.data = absl::Span<uint8_t>(decrypted_frame0_buffer); + crypto.Decrypt(encrypted_frame0, &decrypted_frame0); + EXPECT_EQ(frame0.frame_id, decrypted_frame0.frame_id); + ASSERT_EQ(frame0.data.size(), decrypted_frame0.data.size()); + EXPECT_EQ(0, memcmp(frame0.data.data(), decrypted_frame0.data.data(), + frame0.data.size())); + + EncodedFrame decrypted_frame1; + std::vector<uint8_t> decrypted_frame1_buffer( + FrameCrypto::GetPlaintextSize(encrypted_frame1)); + decrypted_frame1.data = absl::Span<uint8_t>(decrypted_frame1_buffer); + crypto.Decrypt(encrypted_frame1, &decrypted_frame1); + EXPECT_EQ(frame1.frame_id, decrypted_frame1.frame_id); + ASSERT_EQ(frame1.data.size(), decrypted_frame1.data.size()); + EXPECT_EQ(0, memcmp(frame1.data.data(), decrypted_frame1.data.data(), + frame1.data.size())); +} + +} // namespace +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/frame_id.cc b/cast/streaming/frame_id.cc new file mode 100644 index 00000000..bf061fe8 --- /dev/null +++ b/cast/streaming/frame_id.cc @@ -0,0 +1,18 @@ +// Copyright 2016 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/frame_id.h" + +namespace cast { +namespace streaming { + +std::ostream& operator<<(std::ostream& out, const FrameId rhs) { + out << "F"; + if (rhs.is_null()) + return out << "<null>"; + return out << rhs.value(); +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/frame_id.h b/cast/streaming/frame_id.h new file mode 100644 index 00000000..36252422 --- /dev/null +++ b/cast/streaming/frame_id.h @@ -0,0 +1,110 @@ +// Copyright 2016 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. + +#ifndef CAST_STREAMING_FRAME_ID_H_ +#define CAST_STREAMING_FRAME_ID_H_ + +#include <stdint.h> + +#include <sstream> + +#include "cast/streaming/expanded_value_base.h" + +namespace cast { +namespace streaming { + +// Forward declaration (see below). +class FrameId; + +// Convenience operator overloads for logging. +std::ostream& operator<<(std::ostream& out, const FrameId rhs); + +// Unique identifier for a frame in a RTP media stream. FrameIds are truncated +// to 8-bit values in RTP and RTCP headers, and then expanded back by the other +// endpoint when parsing the headers. +// +// Usage example: +// +// // Distance/offset math. +// FrameId first = FrameId::first(); +// FrameId second = first + 1; +// FrameId third = second + 1; +// int64_t offset = third - first; +// FrameId fourth = second + offset; +// +// // Logging convenience. +// OSP_DLOG_INFO << "The current frame is " << fourth; +class FrameId : public ExpandedValueBase<int64_t, FrameId> { + public: + // The "null" FrameId constructor. Represents a FrameId field that has not + // been set and/or a "not applicable" indicator. + constexpr FrameId() : FrameId(std::numeric_limits<int64_t>::min()) {} + + // Allow copy construction and assignment. + constexpr FrameId(const FrameId&) = default; + constexpr FrameId& operator=(const FrameId&) = default; + + // Returns true if this is the special value representing null. + constexpr bool is_null() const { return *this == FrameId(); } + + // Distance operator. + int64_t operator-(FrameId rhs) const { + OSP_DCHECK(!is_null()); + OSP_DCHECK(!rhs.is_null()); + return value_ - rhs.value_; + } + + // Operators to compute advancement by incremental amounts. + FrameId operator+(int64_t rhs) const { + OSP_DCHECK(!is_null()); + return FrameId(value_ + rhs); + } + FrameId operator-(int64_t rhs) const { + OSP_DCHECK(!is_null()); + return FrameId(value_ - rhs); + } + FrameId& operator+=(int64_t rhs) { + OSP_DCHECK(!is_null()); + return (*this = (*this + rhs)); + } + FrameId& operator-=(int64_t rhs) { + OSP_DCHECK(!is_null()); + return (*this = (*this - rhs)); + } + FrameId& operator++() { + OSP_DCHECK(!is_null()); + ++value_; + return *this; + } + FrameId& operator--() { + OSP_DCHECK(!is_null()); + --value_; + return *this; + } + FrameId operator++(int) { + OSP_DCHECK(!is_null()); + return FrameId(value_++); + } + FrameId operator--(int) { + OSP_DCHECK(!is_null()); + return FrameId(value_--); + } + + // The identifier for the first frame in a stream. + static constexpr FrameId first() { return FrameId(0); } + + private: + friend class ExpandedValueBase<int64_t, FrameId>; + friend std::ostream& operator<<(std::ostream& out, const FrameId rhs); + + constexpr explicit FrameId(int64_t value) : ExpandedValueBase(value) {} + + // Accessor used by ostream output function. + constexpr int64_t value() const { return value_; } +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_FRAME_ID_H_ diff --git a/cast/streaming/mock_compound_rtcp_parser_client.h b/cast/streaming/mock_compound_rtcp_parser_client.h new file mode 100644 index 00000000..0b16e599 --- /dev/null +++ b/cast/streaming/mock_compound_rtcp_parser_client.h @@ -0,0 +1,29 @@ +// 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. + +#ifndef CAST_STREAMING_MOCK_COMPOUND_RTCP_PARSER_CLIENT_H_ +#define CAST_STREAMING_MOCK_COMPOUND_RTCP_PARSER_CLIENT_H_ + +#include "cast/streaming/compound_rtcp_parser.h" +#include "gmock/gmock.h" + +namespace cast { +namespace streaming { + +class MockCompoundRtcpParserClient : public CompoundRtcpParser::Client { + public: + MOCK_METHOD1(OnReceiverReferenceTimeAdvanced, + void(openscreen::platform::Clock::time_point reference_time)); + MOCK_METHOD1(OnReceiverReport, void(const RtcpReportBlock& receiver_report)); + MOCK_METHOD0(OnReceiverIndicatesPictureLoss, void()); + MOCK_METHOD2(OnReceiverCheckpoint, + void(FrameId frame_id, std::chrono::milliseconds playout_delay)); + MOCK_METHOD1(OnReceiverHasFrames, void(std::vector<FrameId> acks)); + MOCK_METHOD1(OnReceiverIsMissingPackets, void(std::vector<PacketNack> nacks)); +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_MOCK_COMPOUND_RTCP_PARSER_CLIENT_H_ diff --git a/cast/streaming/ntp_time.cc b/cast/streaming/ntp_time.cc new file mode 100644 index 00000000..d7072baf --- /dev/null +++ b/cast/streaming/ntp_time.cc @@ -0,0 +1,58 @@ +// 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/ntp_time.h" + +#include "util/logging.h" + +using openscreen::platform::Clock; +using std::chrono::duration_cast; + +namespace cast { +namespace streaming { + +namespace { + +// The number of seconds between 1 January 1900 and 1 January 1970. +constexpr NtpSeconds kTimeBetweenNtpEpochAndUnixEpoch{INT64_C(2208988800)}; + +} // namespace + +NtpTimeConverter::NtpTimeConverter(Clock::time_point now, + std::chrono::seconds since_unix_epoch) + : start_time_(now), + since_ntp_epoch_(duration_cast<NtpSeconds>(since_unix_epoch) + + kTimeBetweenNtpEpochAndUnixEpoch) {} + +NtpTimeConverter::~NtpTimeConverter() = default; + +NtpTimestamp NtpTimeConverter::ToNtpTimestamp( + Clock::time_point time_point) const { + const Clock::duration time_since_start = time_point - start_time_; + const auto whole_seconds = duration_cast<NtpSeconds>(time_since_start); + const auto remainder = + duration_cast<NtpFraction>(time_since_start - whole_seconds); + return AssembleNtpTimestamp(since_ntp_epoch_ + whole_seconds, remainder); +} + +Clock::time_point NtpTimeConverter::ToLocalTime(NtpTimestamp timestamp) const { + auto ntp_seconds = NtpSecondsPart(timestamp); + // Year 2036 wrap-around check: If the NTP timestamp appears to be a + // point-in-time before 1970, assume the 2036 wrap-around has occurred, and + // adjust to compensate. + if (ntp_seconds <= kTimeBetweenNtpEpochAndUnixEpoch) { + constexpr NtpSeconds kNtpSecondsPerEra{INT64_C(1) << 32}; + ntp_seconds += kNtpSecondsPerEra; + } + + const auto whole_seconds = ntp_seconds - since_ntp_epoch_; + const auto seconds_since_start = + duration_cast<Clock::duration>(whole_seconds) + start_time_; + const auto remainder = + duration_cast<Clock::duration>(NtpFractionPart(timestamp)); + return seconds_since_start + remainder; +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/ntp_time.h b/cast/streaming/ntp_time.h new file mode 100644 index 00000000..f76dae4a --- /dev/null +++ b/cast/streaming/ntp_time.h @@ -0,0 +1,78 @@ +// 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. + +#ifndef CAST_STREAMING_NTP_TIME_H_ +#define CAST_STREAMING_NTP_TIME_H_ + +#include <stdint.h> + +#include "platform/api/time.h" + +namespace cast { +namespace streaming { + +// NTP timestamps are 64-bit timestamps that consist of two 32-bit parts: 1) The +// number of seconds since 1 January 1900; and 2) The fraction of the second, +// where 0 maps to 0x00000000 and each unit increment represents another 2^-32 +// seconds. +// +// Note that it is part of the design of NTP for the seconds part to roll around +// on 7 February 2036. +using NtpTimestamp = uint64_t; + +// NTP fixed-point time math: Declare two std::chrono::duration types with the +// bit-width necessary to reliably perform all conversions to/from NTP format. +using NtpSeconds = std::chrono::duration<int64_t, std::chrono::seconds::period>; +using NtpFraction = + std::chrono::duration<int64_t, std::ratio<1, INT64_C(0x100000000)>>; + +constexpr NtpSeconds NtpSecondsPart(NtpTimestamp timestamp) { + return NtpSeconds(timestamp >> 32); +} + +constexpr NtpFraction NtpFractionPart(NtpTimestamp timestamp) { + return NtpFraction(timestamp & 0xffffffff); +} + +constexpr NtpTimestamp AssembleNtpTimestamp(NtpSeconds seconds, + NtpFraction fraction) { + return (static_cast<uint64_t>(seconds.count()) << 32) | + static_cast<uint32_t>(fraction.count()); +} + +// Converts between openscreen::platform::Clock::time_points and NtpTimestamps. +// The class is instantiated with the current openscreen::platform::Clock time +// and the current wall clock time, and these are used to determine a fixed +// origin reference point for all conversions. Thus, to avoid introducing +// unintended timing-related behaviors, only one NtpTimeConverter instance +// should be used for converting all the NTP timestamps in the same streaming +// session. +class NtpTimeConverter { + public: + NtpTimeConverter(openscreen::platform::Clock::time_point now, + std::chrono::seconds since_unix_epoch = + openscreen::platform::GetWallTimeSinceUnixEpoch()); + ~NtpTimeConverter(); + + NtpTimestamp ToNtpTimestamp( + openscreen::platform::Clock::time_point time_point) const; + openscreen::platform::Clock::time_point ToLocalTime( + NtpTimestamp timestamp) const; + + private: + // The time point on the platform clock's timeline that corresponds to + // approximately the same time point on the NTP timeline. Note that it is + // acceptable for the granularity of the NTP seconds value to be whole seconds + // here: Both a Cast Streaming Sender and Receiver will assume their clocks + // can be off (with respect to each other) by even a large amount; and all + // that matters is that time ticks forward at a reasonable pace from some + // initial point. + const openscreen::platform::Clock::time_point start_time_; + const NtpSeconds since_ntp_epoch_; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_NTP_TIME_H_ diff --git a/cast/streaming/ntp_time_unittest.cc b/cast/streaming/ntp_time_unittest.cc new file mode 100644 index 00000000..552673c0 --- /dev/null +++ b/cast/streaming/ntp_time_unittest.cc @@ -0,0 +1,110 @@ +// 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/ntp_time.h" + +#include "gtest/gtest.h" + +using openscreen::platform::Clock; +using std::chrono::duration_cast; +using std::chrono::microseconds; +using std::chrono::milliseconds; + +namespace cast { +namespace streaming { + +TEST(NtpTimestampTest, SplitsIntoParts) { + // 1 Jan 1900. + NtpTimestamp timestamp = UINT64_C(0x0000000000000000); + EXPECT_EQ(NtpSeconds::zero(), NtpSecondsPart(timestamp)); + EXPECT_EQ(NtpFraction::zero(), NtpFractionPart(timestamp)); + + // 1 Jan 1900 plus 10 ms. + timestamp = UINT64_C(0x00000000028f5c29); + EXPECT_EQ(NtpSeconds::zero(), NtpSecondsPart(timestamp)); + EXPECT_EQ(milliseconds(10), + duration_cast<milliseconds>(NtpFractionPart(timestamp))); + + // 1 Jan 1970 minus 2^-32 seconds. + timestamp = UINT64_C(0x83aa7e80ffffffff); + EXPECT_EQ(NtpSeconds(INT64_C(2208988800)), NtpSecondsPart(timestamp)); + EXPECT_EQ(NtpFraction(0xffffffff), NtpFractionPart(timestamp)); + + // 2019-03-23 17:25:50.500. + timestamp = UINT64_C(0xe0414d0e80000000); + EXPECT_EQ(NtpSeconds(INT64_C(3762375950)), NtpSecondsPart(timestamp)); + EXPECT_EQ(milliseconds(500), + duration_cast<milliseconds>(NtpFractionPart(timestamp))); +} + +TEST(NtpTimestampTest, AssemblesFromParts) { + // 1 Jan 1900. + NtpTimestamp timestamp = + AssembleNtpTimestamp(NtpSeconds::zero(), NtpFraction::zero()); + EXPECT_EQ(UINT64_C(0x0000000000000000), timestamp); + + // 1 Jan 1900 plus 10 ms. Note that the duration_cast<NtpFraction>(10ms) + // truncates rather than rounds the 10ms value, so the resulting timestamp is + // one fractional tick less than the one found in the SplitsIntoParts test. + // The ~0.4 nanosecond error in the conversion is totally insignificant to a + // live system. + timestamp = AssembleNtpTimestamp( + NtpSeconds::zero(), duration_cast<NtpFraction>(milliseconds(10))); + EXPECT_EQ(UINT64_C(0x00000000028f5c28), timestamp); + + // 1 Jan 1970 minus 2^-32 seconds. + timestamp = AssembleNtpTimestamp(NtpSeconds(INT64_C(2208988799)), + NtpFraction(0xffffffff)); + EXPECT_EQ(UINT64_C(0x83aa7e7fffffffff), timestamp); + + // 2019-03-23 17:25:50.500. + timestamp = + AssembleNtpTimestamp(NtpSeconds(INT64_C(3762375950)), + duration_cast<NtpFraction>(milliseconds(500))); + EXPECT_EQ(UINT64_C(0xe0414d0e80000000), timestamp); +} + +TEST(NtpTimeConverterTest, ConvertsToNtpTimeAndBack) { + // There is an undetermined amount of delay between the sampling of the two + // clocks, but that is accounted for in the design (see class comments). + // Normally, sampling real clocks in unit tests is a recipe for flakiness + // down-the-road. However, if there is flakiness in this test, then some of + // our core assumptions (or the design) about the time math are wrong and + // should be looked into! + const Clock::time_point steady_clock_start = Clock::now(); + const std::chrono::seconds wall_clock_start = + openscreen::platform::GetWallTimeSinceUnixEpoch(); + SCOPED_TRACE(::testing::Message() + << "steady_clock_start.time_since_epoch().count() is " + << steady_clock_start.time_since_epoch().count() + << ", wall_clock_start.count() is " << wall_clock_start.count()); + + const NtpTimeConverter converter(steady_clock_start, wall_clock_start); + + // Convert time points between the start time and 5 seconds later, in 10 ms + // increments. Allow the converted-back time point to be at most 1 clock tick + // off from the original value, but all converted values should always be + // monotonically increasing. + const Clock::time_point end_point = steady_clock_start + milliseconds(5000); + NtpTimestamp last_ntp_timestamp = 0; + Clock::time_point last_converted_back_time_point = Clock::time_point::min(); + for (Clock::time_point t = steady_clock_start; t < end_point; + t += milliseconds(10)) { + const NtpTimestamp ntp_timestamp = converter.ToNtpTimestamp(t); + ASSERT_GT(ntp_timestamp, last_ntp_timestamp); + last_ntp_timestamp = ntp_timestamp; + + const Clock::time_point converted_back_time_point = + converter.ToLocalTime(ntp_timestamp); + ASSERT_GT(converted_back_time_point, last_converted_back_time_point); + last_converted_back_time_point = converted_back_time_point; + + ASSERT_NEAR(t.time_since_epoch().count(), + converted_back_time_point.time_since_epoch().count(), + 1 /* tick */); + } +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/packet_receive_stats_tracker.cc b/cast/streaming/packet_receive_stats_tracker.cc new file mode 100644 index 00000000..0d7f268e --- /dev/null +++ b/cast/streaming/packet_receive_stats_tracker.cc @@ -0,0 +1,81 @@ +// 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/packet_receive_stats_tracker.h" + +#include <algorithm> + +using openscreen::platform::Clock; + +namespace cast { +namespace streaming { + +PacketReceiveStatsTracker::PacketReceiveStatsTracker(int rtp_timebase) + : rtp_timebase_(rtp_timebase) {} + +PacketReceiveStatsTracker::~PacketReceiveStatsTracker() = default; + +void PacketReceiveStatsTracker::OnReceivedValidRtpPacket( + uint16_t sequence_number, + RtpTimeTicks rtp_timestamp, + Clock::time_point arrival_time) { + if (num_rtp_packets_received_ == 0) { + // Since this is the very first packet received, initialize all other + // tracking stats. + num_rtp_packets_received_at_last_report_ = 0; + greatest_sequence_number_ = PacketSequenceNumber(sequence_number); + base_sequence_number_ = greatest_sequence_number_.previous(); + greatest_sequence_number_at_last_report_ = base_sequence_number_; + jitter_ = Clock::duration::zero(); + } else { + // Update the greatest sequence number ever seen. + const auto expanded_sequence_number = + greatest_sequence_number_.Expand(sequence_number); + if (expanded_sequence_number > greatest_sequence_number_) { + greatest_sequence_number_ = expanded_sequence_number; + } + + // Update the interarrival jitter. This is similar to the calculation in + // Appendix A of the RFC 3550 spec (for RTP). + const Clock::duration time_between_arrivals = + arrival_time - last_rtp_packet_arrival_time_; + const auto media_time_difference = + (rtp_timestamp - last_rtp_packet_timestamp_) + .ToDuration<Clock::duration>(rtp_timebase_); + const auto delta = time_between_arrivals - media_time_difference; + const auto absolute_delta = + (delta < decltype(delta)::zero()) ? -delta : delta; + jitter_ += (absolute_delta - jitter_) / 16; + } + + ++num_rtp_packets_received_; + last_rtp_packet_arrival_time_ = arrival_time; + last_rtp_packet_timestamp_ = rtp_timestamp; +} + +void PacketReceiveStatsTracker::PopulateNextReport(RtcpReportBlock* report) { + if (num_rtp_packets_received_ <= 0) { + // None of the packet loss, etc., tracking has valid values yet; so don't + // populate anything. + return; + } + + report->SetPacketFractionLostNumerator( + greatest_sequence_number_ - greatest_sequence_number_at_last_report_, + num_rtp_packets_received_ - num_rtp_packets_received_at_last_report_); + greatest_sequence_number_at_last_report_ = greatest_sequence_number_; + num_rtp_packets_received_at_last_report_ = num_rtp_packets_received_; + + report->SetCumulativePacketsLost( + greatest_sequence_number_ - base_sequence_number_, + num_rtp_packets_received_); + + report->extended_high_sequence_number = + greatest_sequence_number_.lower_32_bits(); + + report->jitter = RtpTimeDelta::FromDuration(jitter_, rtp_timebase_); +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/packet_receive_stats_tracker.h b/cast/streaming/packet_receive_stats_tracker.h new file mode 100644 index 00000000..e579f76f --- /dev/null +++ b/cast/streaming/packet_receive_stats_tracker.h @@ -0,0 +1,107 @@ +// 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. + +#ifndef CAST_STREAMING_PACKET_RECEIVE_STATS_TRACKER_H_ +#define CAST_STREAMING_PACKET_RECEIVE_STATS_TRACKER_H_ + +#include <stdint.h> + +#include "cast/streaming/expanded_value_base.h" +#include "cast/streaming/rtcp_common.h" +#include "cast/streaming/rtp_time.h" +#include "platform/api/time.h" + +namespace cast { +namespace streaming { + +// Maintains statistics for RTP packet arrival timing, jitter, and loss rates; +// and then uses these to compute and set the related fields in a RTCP Receiver +// Report block. +class PacketReceiveStatsTracker { + public: + explicit PacketReceiveStatsTracker(int rtp_timebase); + ~PacketReceiveStatsTracker(); + + // This should be called each time a RTP packet is successfully parsed, + // whether the packet is a duplicate or not. The |sequence_number| and + // |rtp_timestamp| arguments should be the values from the + // RtpPacketParser::ParseResult. |arrival_time| is when the packet was + // received (i.e., right-off the network socket, before any + // processing/parsing). + void OnReceivedValidRtpPacket( + uint16_t sequence_number, + RtpTimeTicks rtp_timestamp, + openscreen::platform::Clock::time_point arrival_time); + + // Populates *only* those fields in the given |report| that pertain to packet + // loss, jitter, and the latest-known RTP packet sequence number. + void PopulateNextReport(RtcpReportBlock* report); + + private: + // Expands the 16-bit raw packet sequence counter values into full-form, + // initially constructed from a "first" value. + class PacketSequenceNumber + : public ExpandedValueBase<int64_t, PacketSequenceNumber> { + public: + constexpr PacketSequenceNumber() + : ExpandedValueBase(std::numeric_limits<int64_t>::min()) {} + constexpr explicit PacketSequenceNumber(uint16_t first_raw_counter_value) + : ExpandedValueBase(static_cast<int64_t>(first_raw_counter_value)) {} + + constexpr bool is_null() const { return *this == PacketSequenceNumber(); } + + constexpr PacketSequenceNumber previous() const { + return PacketSequenceNumber(value_ - 1); + } + + // Distance operator. + constexpr int64_t operator-(PacketSequenceNumber rhs) const { + return value_ - rhs.value_; + } + + private: + friend class ExpandedValueBase<int64_t, PacketSequenceNumber>; + + constexpr explicit PacketSequenceNumber(int64_t value) + : ExpandedValueBase(value) {} + }; + + const int rtp_timebase_; // RTP timestamp ticks per second. + + // Until |num_rtp_packets_received_| is greater than zero, the rest of these + // fields contain invalid values. + int64_t num_rtp_packets_received_ = 0; + int64_t num_rtp_packets_received_at_last_report_; + + // The greatest packet sequence number seen in any RTP packet. + PacketSequenceNumber greatest_sequence_number_; + + // One before the packet sequence number contained in the very first RTP + // packet seen. This is "one before" to simplify the packet count + // calculations. + PacketSequenceNumber base_sequence_number_; + + // The value of |greatest_sequence_number_| when the last call to + // PopulateNextReport() was made. This is used in the computation of the + // packet loss rate between reports. + PacketSequenceNumber greatest_sequence_number_at_last_report_; + + // The time the last RTP packet was received. This is used in the computation + // that updates |jitter_|. + openscreen::platform::Clock::time_point last_rtp_packet_arrival_time_; + + // The RTP timestamp of the last RTP packet received. This is used in the + // computation that updates |jitter_|. + RtpTimeTicks last_rtp_packet_timestamp_; + + // The interarrival jitter. See RFC 3550 spec, section 6.4.1. The Cast + // Streaming spec diverges from the algorithm in the RFC spec in that it uses + // different pieces of timing data to calculate this metric. + openscreen::platform::Clock::duration jitter_; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_PACKET_RECEIVE_STATS_TRACKER_H_ diff --git a/cast/streaming/packet_receive_stats_tracker_unittest.cc b/cast/streaming/packet_receive_stats_tracker_unittest.cc new file mode 100644 index 00000000..1532741f --- /dev/null +++ b/cast/streaming/packet_receive_stats_tracker_unittest.cc @@ -0,0 +1,210 @@ +// 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/packet_receive_stats_tracker.h" + +#include <limits> + +#include "cast/streaming/constants.h" +#include "gtest/gtest.h" + +using openscreen::platform::Clock; + +namespace cast { +namespace streaming { +namespace { + +constexpr int kSomeRtpTimebase = static_cast<int>(kVideoTimebase::den); + +// Returns a RtcpReportBlock with all fields set to known values to see how the +// fields are modified by functions called during the tests. +RtcpReportBlock GetSentinel() { + RtcpReportBlock report; + report.ssrc = Ssrc{0x1337beef}; + report.packet_fraction_lost_numerator = -999; + report.cumulative_packets_lost = -0x1337cafe; + report.extended_high_sequence_number = 0x98765432; + report.jitter = + RtpTimeDelta::FromTicks(std::numeric_limits<int64_t>::max() - 42); + report.last_status_report_id = StatusReportId{2222222222}; + report.delay_since_last_report = RtcpReportBlock::Delay(-0x3550641); + return report; +} + +// Run gtest expectations, that no fields were changed. +#define EXPECT_FIELDS_NOT_POPULATED(x) \ + do { \ + const RtcpReportBlock sentinel = GetSentinel(); \ + EXPECT_EQ(sentinel.ssrc, (x).ssrc); \ + EXPECT_EQ(sentinel.packet_fraction_lost_numerator, \ + (x).packet_fraction_lost_numerator); \ + EXPECT_EQ(sentinel.cumulative_packets_lost, (x).cumulative_packets_lost); \ + EXPECT_EQ(sentinel.extended_high_sequence_number, \ + (x).extended_high_sequence_number); \ + EXPECT_EQ(sentinel.jitter, (x).jitter); \ + EXPECT_EQ(sentinel.last_status_report_id, (x).last_status_report_id); \ + EXPECT_EQ(sentinel.delay_since_last_report, (x).delay_since_last_report); \ + } while (false) + +// Run gtest expectations, that only the fields changed by +// PacketReceiveStatsTracker::PopulateNextReport() were changed. +#define EXPECT_FIELDS_POPULATED(x) \ + do { \ + const RtcpReportBlock sentinel = GetSentinel(); \ + /* Fields that should remain untouched by PopulateNextReport(). */ \ + EXPECT_EQ(sentinel.ssrc, (x).ssrc); \ + EXPECT_EQ(sentinel.last_status_report_id, (x).last_status_report_id); \ + EXPECT_EQ(sentinel.delay_since_last_report, (x).delay_since_last_report); \ + /* Fields that should have changed.*/ \ + EXPECT_NE(sentinel.packet_fraction_lost_numerator, \ + (x).packet_fraction_lost_numerator); \ + EXPECT_NE(sentinel.cumulative_packets_lost, (x).cumulative_packets_lost); \ + EXPECT_NE(sentinel.extended_high_sequence_number, \ + (x).extended_high_sequence_number); \ + EXPECT_NE(sentinel.jitter, (x).jitter); \ + } while (false) + +TEST(PacketReceiveStatsTrackerTest, DoesNotPopulateReportWithoutData) { + PacketReceiveStatsTracker tracker(kSomeRtpTimebase); + RtcpReportBlock report = GetSentinel(); + tracker.PopulateNextReport(&report); + EXPECT_FIELDS_NOT_POPULATED(report); +} + +TEST(PacketReceiveStatsTrackerTest, PopulatesReportWithOnePacketTracked) { + constexpr uint16_t kSequenceNumber = 1234; + constexpr RtpTimeTicks kRtpTimestamp = + RtpTimeTicks() + RtpTimeDelta::FromTicks(42); + constexpr auto kArrivalTime = + Clock::time_point() + std::chrono::seconds(3600); + + PacketReceiveStatsTracker tracker(kSomeRtpTimebase); + tracker.OnReceivedValidRtpPacket(kSequenceNumber, kRtpTimestamp, + kArrivalTime); + + RtcpReportBlock report = GetSentinel(); + tracker.PopulateNextReport(&report); + EXPECT_FIELDS_POPULATED(report); + EXPECT_EQ(0, report.packet_fraction_lost_numerator); + EXPECT_EQ(0, report.cumulative_packets_lost); + EXPECT_EQ(kSequenceNumber, report.extended_high_sequence_number); + EXPECT_EQ(RtpTimeDelta(), report.jitter); +} + +TEST(PacketReceiveStatsTrackerTest, WhenReceivingAllPackets) { + // Set the first sequence number such that wraparound is going to be tested. + constexpr uint16_t kFirstSequenceNumber = + std::numeric_limits<uint16_t>::max() - 2; + constexpr RtpTimeTicks kFirstRtpTimestamp = + RtpTimeTicks() + RtpTimeDelta::FromTicks(42); + constexpr auto kFirstArrivalTime = + Clock::time_point() + std::chrono::seconds(3600); + + PacketReceiveStatsTracker tracker(kSomeRtpTimebase); + + // Record 10 packets arrived exactly one second apart with media timestamps + // also exactly one second apart. + for (int i = 0; i < 10; ++i) { + tracker.OnReceivedValidRtpPacket( + kFirstSequenceNumber + i, + kFirstRtpTimestamp + RtpTimeDelta::FromTicks(kSomeRtpTimebase) * i, + kFirstArrivalTime + std::chrono::seconds(i)); + } + + RtcpReportBlock report = GetSentinel(); + tracker.PopulateNextReport(&report); + EXPECT_FIELDS_POPULATED(report); + + // Nothing should indicate to the tracker that any packets were dropped. + EXPECT_EQ(0, report.packet_fraction_lost_numerator); + EXPECT_EQ(0, report.cumulative_packets_lost); + + // The |extended_high_sequence_number| should reflect the wraparound of the + // 16-bit counter value. + EXPECT_EQ(uint32_t{65542}, report.extended_high_sequence_number); + + // There should be zero jitter, based on the timing information that was given + // for each RTP packet. + EXPECT_EQ(RtpTimeDelta(), report.jitter); +} + +TEST(PacketReceiveStatsTrackerTest, WhenReceivingAboutHalfThePackets) { + constexpr uint16_t kFirstSequenceNumber = 3; + constexpr RtpTimeTicks kFirstRtpTimestamp = + RtpTimeTicks() + RtpTimeDelta::FromTicks(99); + constexpr auto kFirstArrivalTime = + Clock::time_point() + std::chrono::seconds(8888); + + PacketReceiveStatsTracker tracker(kSomeRtpTimebase); + + // Record 10 packet arrivals whose sequence numbers step by 2, which should + // indicate half of the packets didn't arrive. + // + // Ten arrived: 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 + // Nine inferred missing: 2, 4, 6, 8, 10, 12, 14, 16, 18 + for (int i = 0; i < 10; ++i) { + tracker.OnReceivedValidRtpPacket( + kFirstSequenceNumber + (i * 2 + 1), + kFirstRtpTimestamp + RtpTimeDelta::FromTicks(kSomeRtpTimebase) * i, + kFirstArrivalTime + std::chrono::seconds(i)); + } + + RtcpReportBlock report = GetSentinel(); + tracker.PopulateNextReport(&report); + EXPECT_FIELDS_POPULATED(report); + EXPECT_EQ(121, report.packet_fraction_lost_numerator); + EXPECT_EQ(9, report.cumulative_packets_lost); + EXPECT_EQ(uint32_t{22}, report.extended_high_sequence_number); + // There should be zero jitter, based on the timing information that was given + // for each RTP packet. + EXPECT_EQ(RtpTimeDelta(), report.jitter); +} + +TEST(PacketReceiveStatsTrackerTest, ComputesJitterCorrectly) { + constexpr uint16_t kFirstSequenceNumber = 3; + constexpr RtpTimeTicks kFirstRtpTimestamp = + RtpTimeTicks() + RtpTimeDelta::FromTicks(99); + constexpr auto kFirstArrivalTime = + Clock::time_point() + std::chrono::seconds(8888); + + // Record 100 packet arrivals, one second apart, where each packet's RTP + // timestamps are progressing 2 seconds forward. Thus, the jitter calculation + // should gradually converge towards a difference of one second. + constexpr auto kTrueJitter = + std::chrono::duration_cast<Clock::duration>(std::chrono::seconds(1)); + PacketReceiveStatsTracker tracker(kSomeRtpTimebase); + Clock::duration last_diff = Clock::duration::max(); + for (int i = 0; i < 100; ++i) { + tracker.OnReceivedValidRtpPacket( + kFirstSequenceNumber + i, + kFirstRtpTimestamp + + RtpTimeDelta::FromTicks(kSomeRtpTimebase) * (i * 2), + kFirstArrivalTime + std::chrono::seconds(i)); + + // Expect that the jitter is becoming closer to the actual value in each + // iteration. + RtcpReportBlock report; + tracker.PopulateNextReport(&report); + const auto diff = kTrueJitter - report.jitter.ToDuration<Clock::duration>( + kSomeRtpTimebase); + EXPECT_LT(diff, last_diff); + last_diff = diff; + } + + // Because the jitter calculation is a weighted moving average, and also + // because the timebase has to be converted here, the metric might not ever + // become exactly kTrueJitter. Ensure that it has converged reasonably close + // to that value. + RtcpReportBlock report; + tracker.PopulateNextReport(&report); + const auto diff = + kTrueJitter - report.jitter.ToDuration<Clock::duration>(kSomeRtpTimebase); + constexpr auto kMaxDiffAtEnd = + std::chrono::duration_cast<Clock::duration>(std::chrono::milliseconds(2)); + EXPECT_NEAR(0, diff.count(), kMaxDiffAtEnd.count()); +} + +} // namespace +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/packet_util.cc b/cast/streaming/packet_util.cc new file mode 100644 index 00000000..cf43ac14 --- /dev/null +++ b/cast/streaming/packet_util.cc @@ -0,0 +1,44 @@ +// 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/packet_util.h" + +#include "cast/streaming/rtcp_common.h" +#include "cast/streaming/rtp_defines.h" + +using openscreen::ReadBigEndian; + +namespace cast { +namespace streaming { + +std::pair<ApparentPacketType, Ssrc> InspectPacketForRouting( + absl::Span<const uint8_t> packet) { + // Check for RTP packets first, since they are more frequent. + if (packet.size() >= kRtpPacketMinValidSize && + packet[0] == kRtpRequiredFirstByte && + IsRtpPayloadType(packet[1] & kRtpPayloadTypeMask)) { + constexpr int kOffsetToSsrcField = 8; + return std::make_pair( + ApparentPacketType::RTP, + Ssrc{ReadBigEndian<uint32_t>(packet.data() + kOffsetToSsrcField)}); + } + + // While RTCP packets are valid if they consist of just the RTCP Common + // Header, all the RTCP packet types processed by this implementation will + // also have a SSRC field immediately following the header. This is important + // for routing the packet to the correct parser instance. + constexpr int kRtcpPacketMinAcceptableSize = + kRtcpCommonHeaderSize + sizeof(uint32_t); + if (packet.size() >= kRtcpPacketMinAcceptableSize && + RtcpCommonHeader::Parse(packet).has_value()) { + return std::make_pair( + ApparentPacketType::RTCP, + Ssrc{ReadBigEndian<uint32_t>(packet.data() + kRtcpCommonHeaderSize)}); + } + + return std::make_pair(ApparentPacketType::UNKNOWN, Ssrc{0}); +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/packet_util.h b/cast/streaming/packet_util.h new file mode 100644 index 00000000..869b917e --- /dev/null +++ b/cast/streaming/packet_util.h @@ -0,0 +1,62 @@ +// 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. + +#ifndef CAST_STREAMING_PACKET_UTIL_H_ +#define CAST_STREAMING_PACKET_UTIL_H_ + +#include <utility> + +#include "absl/types/span.h" +#include "cast/streaming/ssrc.h" +#include "util/big_endian.h" + +namespace cast { +namespace streaming { + +// Reads a field from the start of the given span and advances the span to point +// just after the field. +template <typename Integer> +inline Integer ConsumeField(absl::Span<const uint8_t>* in) { + const Integer result = openscreen::ReadBigEndian<Integer>(in->data()); + in->remove_prefix(sizeof(Integer)); + return result; +} + +// Writes a field at the start of the given span and advances the span to point +// just after the field. +template <typename Integer> +inline void AppendField(Integer value, absl::Span<uint8_t>* out) { + openscreen::WriteBigEndian<Integer>(value, out->data()); + out->remove_prefix(sizeof(Integer)); +} + +// Returns a bitmask for a field having the given number of bits. For example, +// FieldBitmask<uint8_t>(5) returns 0b00011111. +template <typename Integer> +constexpr Integer FieldBitmask(unsigned field_size_in_bits) { + return (Integer{1} << field_size_in_bits) - 1; +} + +// Reserves |num_bytes| from the beginning of the given span, returning the +// reserved space. +inline absl::Span<uint8_t> ReserveSpace(int num_bytes, + absl::Span<uint8_t>* out) { + const absl::Span<uint8_t> reserved = out->subspan(0, num_bytes); + out->remove_prefix(num_bytes); + return reserved; +} + +// Performs a quick-scan of the packet data for the purposes of routing it to an +// appropriate parser. Identifies whether the packet is a RTP packet, RTCP +// packet, or unknown; and provides the originator's SSRC. This only performs a +// very quick scan of the packet data, and does not guarantee that a full parse +// will later succeed. +enum class ApparentPacketType { UNKNOWN, RTP, RTCP }; +std::pair<ApparentPacketType, Ssrc> InspectPacketForRouting( + absl::Span<const uint8_t> packet); + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_PACKET_UTIL_H_ diff --git a/cast/streaming/packet_util_unittest.cc b/cast/streaming/packet_util_unittest.cc new file mode 100644 index 00000000..1f1f095d --- /dev/null +++ b/cast/streaming/packet_util_unittest.cc @@ -0,0 +1,185 @@ +// 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/packet_util.h" + +#include "absl/types/span.h" +#include "gtest/gtest.h" + +namespace cast { +namespace streaming { +namespace { + +// Tests that a simple RTCP packet containing only a Sender Report can be +// identified. +TEST(PacketUtilTest, InspectsRtcpPacketFromSender) { + // clang-format off + const uint8_t kSenderReportPacket[] = { + 0b10000000, // Version=2, Padding=no, ItemCount=0. + 200, // RTCP Packet type. + 0x00, 0x06, // Length of remainder of packet, in 32-bit words. + 1, 2, 3, 4, // SSRC of sender. + 0xe0, 0x73, 0x2e, 0x54, // NTP Timestamp (late evening on 2019-04-30). + 0x80, 0x00, 0x00, 0x00, + 0x00, 0x14, 0x99, 0x70, // RTP Timestamp (15 seconds, 90kHz timebase). + 0x00, 0x00, 0x01, 0xff, // Sender's Packet Count. + 0x00, 0x07, 0x11, 0x0d, // Sender's Octet Count. + }; + // clang-format on + const Ssrc kSenderSsrc = 0x01020304; + + const auto result = InspectPacketForRouting(kSenderReportPacket); + EXPECT_EQ(ApparentPacketType::RTCP, result.first); + EXPECT_EQ(kSenderSsrc, result.second); +} + +// Tests that compound RTCP packets containing a Receiver Report and/or a Cast +// Feedback message can be identified. +TEST(PacketUtilTest, InspectsRtcpPacketFromReceiver) { + // clang-format off + const uint8_t kReceiverReportPacket[] = { + 0b10000001, // Version=2, Padding=no, ItemCount=1. + 201, // RTCP Packet type. + 0x00, 0x01, // Length of remainder of packet, in 32-bit words. + 9, 8, 7, 6, // SSRC of receiver. + }; + const uint8_t kCastFeedbackPacket[] = { + // Cast Feedback + 0b10000000 | 15, // Version=2, Padding=no, Subtype=15. + 206, // RTCP Packet type byte. + 0x00, 0x04, // Length of remainder of packet, in 32-bit words. + 9, 8, 7, 6, // SSRC of receiver. + 1, 2, 3, 4, // SSRC of sender. + 'C', 'A', 'S', 'T', + 0x0a, // Checkpoint Frame ID (lower 8 bits). + 0x00, // Number of "Loss Fields" + 0x00, 0x28, // Current Playout Delay in milliseconds. + }; + // clang-format on + const Ssrc kReceiverSsrc = 0x09080706; + + { + const auto result = InspectPacketForRouting(kReceiverReportPacket); + EXPECT_EQ(ApparentPacketType::RTCP, result.first); + EXPECT_EQ(kReceiverSsrc, result.second); + } + + { + const auto result = InspectPacketForRouting(kCastFeedbackPacket); + EXPECT_EQ(ApparentPacketType::RTCP, result.first); + EXPECT_EQ(kReceiverSsrc, result.second); + } + + const absl::Span<const uint8_t> kCompoundCombinations[2][2] = { + {kReceiverReportPacket, kCastFeedbackPacket}, + {kCastFeedbackPacket, kReceiverReportPacket}, + }; + for (const auto& combo : kCompoundCombinations) { + uint8_t compound_packet[sizeof(kReceiverReportPacket) + + sizeof(kCastFeedbackPacket)]; + memcpy(compound_packet, combo[0].data(), combo[0].size()); + memcpy(compound_packet + combo[0].size(), combo[1].data(), combo[1].size()); + + const auto result = InspectPacketForRouting(compound_packet); + EXPECT_EQ(ApparentPacketType::RTCP, result.first); + EXPECT_EQ(kReceiverSsrc, result.second); + } +} + +// Tests that a RTP packet can be identified. +TEST(PacketUtilTest, InspectsRtpPacket) { + // clang-format off + const uint8_t kInput[] = { + 0b10000000, // Version/Padding byte. + 96, // Payload type byte. + 0xbe, 0xef, // Sequence number. + 9, 8, 7, 6, // RTP timestamp. + 1, 2, 3, 4, // SSRC. + 0b10000000, // Is key frame, no extensions. + 5, // Frame ID. + 0xa, 0xb, // Packet ID. + 0xa, 0xc, // Max packet ID. + 0xf, 0xe, 0xd, 0xc, 0xb, 0xa, 0x9, 0x8, // Payload. + }; + // clang-format on + const Ssrc kSenderSsrc = 0x01020304; + + const auto result = InspectPacketForRouting(kInput); + EXPECT_EQ(ApparentPacketType::RTP, result.first); + EXPECT_EQ(kSenderSsrc, result.second); +} + +// Tests that a RTP packet with the "127 payload type" hack can be identified as +// valid. See comments in rtp_defines.h for the RtpPayloadType enum definition, +// for further details. +TEST(PacketUtilTest, InspectsAndroidAudioRtpPacket) { + // clang-format off + const uint8_t kInput[] = { + 0b10000000, // Version/Padding byte. + 127, // Payload type byte. + 0xbe, 0xef, // Sequence number. + 9, 8, 7, 6, // RTP timestamp. + 1, 2, 3, 4, // SSRC. + 0b10000000, // Is key frame, no extensions. + 5, // Frame ID. + 0xa, 0xb, // Packet ID. + 0xa, 0xc, // Max packet ID. + 0xf, 0xe, 0xd, 0xc, 0xb, 0xa, 0x9, 0x8, // Payload. + }; + // clang-format on + const Ssrc kSenderSsrc = 0x01020304; + + const auto result = InspectPacketForRouting(kInput); + EXPECT_EQ(ApparentPacketType::RTP, result.first); + EXPECT_EQ(kSenderSsrc, result.second); +} + +// Tests that a malformed RTP packet can be identified. +TEST(PacketUtilTest, InspectsMalformedRtpPacket) { + // clang-format off + const uint8_t kInput[] = { + 0b11000000, // BAD: Version/Padding byte. + 96, // Payload type byte. + 0xbe, 0xef, // Sequence number. + 9, 8, 7, 6, // RTP timestamp. + 1, 2, 3, 4, // SSRC. + 0b10000000, // Is key frame, no extensions. + 5, // Frame ID. + 0xa, 0xb, // Packet ID. + 0xa, 0xc, // Max packet ID. + 0xf, 0xe, 0xd, 0xc, 0xb, 0xa, 0x9, 0x8, // Payload. + }; + // clang-format on + + const auto result = InspectPacketForRouting(kInput); + EXPECT_EQ(ApparentPacketType::UNKNOWN, result.first); +} + +// Tests that an empty packet is classified as unknown. +TEST(PacketUtilTest, InspectsEmptyPacket) { + const uint8_t kInput[] = {}; + + const auto result = + InspectPacketForRouting(absl::Span<const uint8_t>(kInput, 0)); + EXPECT_EQ(ApparentPacketType::UNKNOWN, result.first); +} + +// Tests that a packet with garbage is classified as unknown. +TEST(PacketUtilTest, InspectsGarbagePacket) { + // clang-format off + const uint8_t kInput[] = { + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, + }; + // clang-format on + + const auto result = InspectPacketForRouting(kInput); + EXPECT_EQ(ApparentPacketType::UNKNOWN, result.first); +} + +} // namespace +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/receiver.cc b/cast/streaming/receiver.cc new file mode 100644 index 00000000..73a37eb2 --- /dev/null +++ b/cast/streaming/receiver.cc @@ -0,0 +1,485 @@ +// 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/receiver.h" + +#include <algorithm> +#include <functional> + +#include "absl/types/span.h" +#include "cast/streaming/constants.h" +#include "cast/streaming/receiver_packet_router.h" +#include "util/logging.h" +#include "util/std_util.h" + +using openscreen::platform::Clock; + +using std::chrono::duration_cast; +using std::chrono::microseconds; +using std::chrono::milliseconds; + +namespace cast { +namespace streaming { + +// Conveniences for ensuring logging output includes the SSRC of the Receiver, +// to help distinguish one out of multiple instances in a Cast Streaming +// session. +// +// TODO(miu): Replace RECEIVER_VLOG's with trace event logging once the tracing +// infrastructure is ready. +#define RECEIVER_LOG(level) OSP_LOG_##level << "[SSRC:" << ssrc() << "] " +#define RECEIVER_VLOG OSP_VLOG << "[SSRC:" << ssrc() << "] " + +Receiver::Receiver(Environment* environment, + ReceiverPacketRouter* packet_router, + const cast::streaming::SessionConfig& config, + std::chrono::milliseconds initial_target_playout_delay) + : now_(environment->now_function()), + packet_router_(packet_router), + rtcp_session_(config.sender_ssrc, config.receiver_ssrc, now_()), + rtcp_parser_(&rtcp_session_), + rtcp_builder_(&rtcp_session_), + stats_tracker_(config.rtp_timebase), + rtp_parser_(config.sender_ssrc), + rtp_timebase_(config.rtp_timebase), + crypto_(config.aes_secret_key, config.aes_iv_mask), + rtcp_buffer_capacity_(environment->GetMaxPacketSize()), + rtcp_buffer_(new uint8_t[rtcp_buffer_capacity_]), + rtcp_alarm_(environment->now_function(), environment->task_runner()), + smoothed_clock_offset_(ClockDriftSmoother::kDefaultTimeConstant), + consumption_alarm_(environment->now_function(), + environment->task_runner()) { + OSP_DCHECK(packet_router_); + OSP_DCHECK_EQ(checkpoint_frame(), FrameId::first() - 1); + OSP_CHECK_GT(rtcp_buffer_capacity_, 0); + OSP_CHECK(rtcp_buffer_); + + rtcp_builder_.SetPlayoutDelay(initial_target_playout_delay); + playout_delay_changes_.emplace_back(FrameId::first() - 1, + initial_target_playout_delay); + + packet_router_->OnReceiverCreated(rtcp_session_.sender_ssrc(), this); +} + +Receiver::~Receiver() { + packet_router_->OnReceiverDestroyed(rtcp_session_.sender_ssrc()); +} + +void Receiver::SetConsumer(Consumer* consumer) { + consumer_ = consumer; + ScheduleFrameReadyCheck(); +} + +void Receiver::SetPlayerProcessingTime(Clock::duration needed_time) { + player_processing_time_ = std::max(Clock::duration::zero(), needed_time); +} + +void Receiver::RequestKeyFrame() { + if (!last_key_frame_received_.is_null() && + last_frame_consumed_ >= last_key_frame_received_ && + !rtcp_builder_.is_picture_loss_indicator_set()) { + rtcp_builder_.SetPictureLossIndicator(true); + SendRtcp(); + } +} + +int Receiver::AdvanceToNextFrame() { + const FrameId immediate_next_frame = last_frame_consumed_ + 1; + + // Scan the queue for the next frame that should be consumed. Typically, this + // is the very next frame; but if it is incomplete and already late for + // playout, consider skipping-ahead. + for (FrameId f = immediate_next_frame; f <= latest_frame_expected_; ++f) { + PendingFrame& entry = GetQueueEntry(f); + if (entry.collector.is_complete()) { + const EncryptedFrame& encrypted_frame = + entry.collector.PeekAtAssembledFrame(); + if (f == immediate_next_frame) { // Typical case. + RECEIVER_VLOG << "AdvanceToNextFrame: Next in sequence (" << f << ')'; + return FrameCrypto::GetPlaintextSize(encrypted_frame); + } + if (encrypted_frame.dependency != EncodedFrame::DEPENDS_ON_ANOTHER) { + // Found a frame after skipping past some frames. Drop the ones being + // skipped, advancing |last_frame_consumed_| before returning. + RECEIVER_VLOG << "AdvanceToNextFrame: Skipping-ahead → " << f; + DropAllFramesBefore(f); + return FrameCrypto::GetPlaintextSize(encrypted_frame); + } + // Conclusion: The frame in the current queue entry is complete, but + // depends on a prior incomplete frame. Continue scanning... + } + + // Do not consider skipping past this frame if its estimated capture time is + // unknown. The implication here is that, if |estimated_capture_time| is + // set, the Receiver also knows whether any target playout delay changes + // were communicated from the Sender in the frame's first RTP packet. + if (!entry.estimated_capture_time) { + break; + } + + // If this incomplete frame is not yet late for playout, simply wait for the + // rest of its packets to come in. However, do schedule a check to + // re-examine things at the time it would become a late frame, to possibly + // skip-over it. + const auto playout_time = + *entry.estimated_capture_time + ResolveTargetPlayoutDelay(f); + if (playout_time > (now_() + player_processing_time_)) { + ScheduleFrameReadyCheck(playout_time); + break; + } + } + + RECEIVER_VLOG << "AdvanceToNextFrame: No frames ready. Last consumed was " + << last_frame_consumed_ << '.'; + return kNoFramesReady; +} + +EncodedFrame Receiver::ConsumeNextFrame(absl::Span<uint8_t> buffer) { + // Assumption: The required call to AdvanceToNextFrame() ensures that + // |last_frame_consumed_| is set to one before the frame to be consumed here. + const FrameId frame_id = last_frame_consumed_ + 1; + OSP_CHECK_LE(frame_id, checkpoint_frame()); + + // Decrypt the frame, populating the given output |frame|. + PendingFrame& entry = GetQueueEntry(frame_id); + OSP_DCHECK(entry.collector.is_complete()); + EncodedFrame frame; + frame.data = buffer; + crypto_.Decrypt(entry.collector.PeekAtAssembledFrame(), &frame); + OSP_DCHECK(entry.estimated_capture_time); + frame.reference_time = + *entry.estimated_capture_time + ResolveTargetPlayoutDelay(frame_id); + + RECEIVER_VLOG + << "ConsumeNextFrame → " << frame.frame_id << ": " << frame.data.size() + << " payload bytes, RTP Timestamp " + << frame.rtp_timestamp.ToTimeSinceOrigin<microseconds>(rtp_timebase_) + .count() + << " µs, to play-out " + << duration_cast<microseconds>(frame.reference_time - now_()).count() + << " µs from now."; + + entry.Reset(); + last_frame_consumed_ = frame_id; + + // Ensure the Consumer is notified if there are already more frames ready for + // consumption, and it hasn't explicitly called AdvanceToNextFrame() to check + // for itself. + ScheduleFrameReadyCheck(); + + return frame; +} + +void Receiver::OnReceivedRtpPacket(Clock::time_point arrival_time, + std::vector<uint8_t> packet) { + const absl::optional<RtpPacketParser::ParseResult> part = + rtp_parser_.Parse(packet); + if (!part) { + RECEIVER_LOG(WARN) << "Parsing of " << packet.size() + << " bytes as an RTP packet failed."; + return; + } + stats_tracker_.OnReceivedValidRtpPacket(part->sequence_number, + part->rtp_timestamp, arrival_time); + + // Ignore packets for frames the Receiver is no longer interested in. + if (part->frame_id <= checkpoint_frame()) { + return; + } + + // Extend the range of frames known to this Receiver, within the capacity of + // this Receiver's queue. Prepare the FrameCollectors to receive any + // newly-discovered frames. + if (part->frame_id > latest_frame_expected_) { + const FrameId max_allowed_frame_id = + last_frame_consumed_ + kMaxUnackedFrames; + if (part->frame_id > max_allowed_frame_id) { + RECEIVER_VLOG << "Dropping RTP packet for " << part->frame_id + << ": Too many frames are already in-flight."; + return; + } + do { + ++latest_frame_expected_; + GetQueueEntry(latest_frame_expected_) + .collector.set_frame_id(latest_frame_expected_); + } while (latest_frame_expected_ < part->frame_id); + RECEIVER_VLOG << "Advanced latest frame expected to " + << latest_frame_expected_; + } + + // Start-up edge case: Blatantly drop the first packet of all frames until the + // Receiver has processed at least one Sender Report containing the necessary + // clock-drift and lip-sync information (see OnReceivedRtcpPacket()). This is + // an inescapable data dependency. Note that this special case should almost + // never trigger, since a well-behaving Sender will send the first Sender + // Report RTCP packet before any of the RTP packets. + if (!last_sender_report_ && part->packet_id == FramePacketId{0}) { + RECEIVER_LOG(WARN) << "Dropping packet 0 of frame " << part->frame_id + << " because it arrived before the first Sender Report."; + // Note: The Sender will have to re-transmit this dropped packet after the + // Sender Report to allow the Receiver to move forward. + return; + } + + PendingFrame& pending_frame = GetQueueEntry(part->frame_id); + FrameCollector& collector = pending_frame.collector; + if (collector.is_complete()) { + // An extra, redundant |packet| was received. Do nothing since the frame was + // already complete. + return; + } + + if (!collector.CollectRtpPacket(*part, &packet)) { + return; // Bad data in the parsed packet. Ignore it. + } + + // The first packet in a frame contains timing information critical for + // computing this frame's (and all future frames') playout time. Process that, + // but only once. + if (part->packet_id == FramePacketId{0} && + !pending_frame.estimated_capture_time) { + // Estimate the original capture time of this frame (at the Sender), in + // terms of the Receiver's clock: First, start with a reference time point + // from the Sender's clock (the one from the last Sender Report). Then, + // translate it into the equivalent reference time point in terms of the + // Receiver's clock by applying the measured offset between the two clocks. + // Finally, apply the RTP timestamp difference between the Sender Report and + // this frame to determine what the original capture time of this frame was. + pending_frame.estimated_capture_time = + last_sender_report_->reference_time + smoothed_clock_offset_.Current() + + (part->rtp_timestamp - last_sender_report_->rtp_timestamp) + .ToDuration<Clock::duration>(rtp_timebase_); + + // If a target playout delay change was included in this packet, record it. + if (part->new_playout_delay > milliseconds::zero()) { + RECEIVER_VLOG << "Target playout delay changes to " + << part->new_playout_delay.count() << " ms, as of " + << part->frame_id; + RecordNewTargetPlayoutDelay(part->frame_id, part->new_playout_delay); + } + + // Now that the estimated capture time is known, other frames may have just + // become ready, per the frame-skipping logic in AdvanceToNextFrame(). + ScheduleFrameReadyCheck(); + } + + if (!collector.is_complete()) { + return; // Wait for the rest of the packets to come in. + } + const EncryptedFrame& encrypted_frame = collector.PeekAtAssembledFrame(); + + // Whenever a key frame has been received, the decoder has what it needs to + // recover. In this case, clear the PLI condition. + if (encrypted_frame.dependency == EncryptedFrame::KEY_FRAME) { + rtcp_builder_.SetPictureLossIndicator(false); + last_key_frame_received_ = part->frame_id; + } + + // If this just-completed frame is the one right after the checkpoint frame, + // advance the checkpoint forward. + if (part->frame_id == (checkpoint_frame() + 1)) { + AdvanceCheckpoint(part->frame_id); + } + + // Since a frame has become complete, schedule a check to see whether this or + // any other frames have become ready for consumption. + ScheduleFrameReadyCheck(); +} + +void Receiver::OnReceivedRtcpPacket(Clock::time_point arrival_time, + std::vector<uint8_t> packet) { + absl::optional<SenderReportParser::SenderReportWithId> parsed_report = + rtcp_parser_.Parse(packet); + if (!parsed_report) { + RECEIVER_LOG(WARN) << "Parsing of " << packet.size() + << " bytes as an RTCP packet failed."; + return; + } + last_sender_report_ = std::move(parsed_report); + last_sender_report_arrival_time_ = arrival_time; + + // Measure the offset between the Sender's clock and the Receiver's Clock. + // This will be used to translate reference timestamps from the Sender into + // timestamps that represent the exact same moment in time at the Receiver. + // + // Note: Due to design limitations in the Cast Streaming spec, the Receiver + // has no way to compute how long it took the Sender Report to travel over the + // network. The calculation here just ignores that, and so the + // |measured_offset| below will be larger than the true value by that amount. + // This will have the effect of a later-than-configured playout delay. + const Clock::duration measured_offset = + arrival_time - last_sender_report_->reference_time; + smoothed_clock_offset_.Update(arrival_time, measured_offset); + RECEIVER_VLOG + << "Received Sender Report: Local clock is ahead of Sender's by " + << duration_cast<microseconds>(smoothed_clock_offset_.Current()).count() + << " µs (minus one-way network transit time)."; + + RtcpReportBlock report; + report.ssrc = rtcp_session_.sender_ssrc(); + stats_tracker_.PopulateNextReport(&report); + report.last_status_report_id = last_sender_report_->report_id; + report.SetDelaySinceLastReport(now_() - last_sender_report_arrival_time_); + rtcp_builder_.IncludeReceiverReportInNextPacket(report); + + SendRtcp(); +} + +void Receiver::SendRtcp() { + // Collect ACK/NACK feedback for all active frames in the queue. + std::vector<PacketNack> packet_nacks; + std::vector<FrameId> frame_acks; + for (FrameId f = checkpoint_frame() + 1; f <= latest_frame_expected_; ++f) { + const FrameCollector& collector = GetQueueEntry(f).collector; + if (collector.is_complete()) { + frame_acks.push_back(f); + } else { + collector.GetMissingPackets(&packet_nacks); + } + } + + // Build and send a compound RTCP packet. + const bool no_nacks = packet_nacks.empty(); + rtcp_builder_.IncludeFeedbackInNextPacket(std::move(packet_nacks), + std::move(frame_acks)); + last_rtcp_send_time_ = now_(); + packet_router_->SendRtcpPacket(rtcp_builder_.BuildPacket( + last_rtcp_send_time_, + absl::Span<uint8_t>(rtcp_buffer_.get(), rtcp_buffer_capacity_))); + RECEIVER_VLOG << "Sent RTCP packet."; + + // Schedule the automatic sending of another RTCP packet, if this method is + // not called within some bounded amount of time. While incomplete frames + // exist in the queue, send RTCP packets (with ACK/NACK feedback) frequently. + // When there are no incomplete frames, use a longer "keepalive" interval. + const Clock::duration interval = + (no_nacks ? kRtcpReportInterval : kNackFeedbackInterval); + rtcp_alarm_.Schedule(std::bind(&Receiver::SendRtcp, this), + last_rtcp_send_time_ + interval); +} + +const Receiver::PendingFrame& Receiver::GetQueueEntry(FrameId frame_id) const { + return const_cast<Receiver*>(this)->GetQueueEntry(frame_id); +} + +Receiver::PendingFrame& Receiver::GetQueueEntry(FrameId frame_id) { + return pending_frames_[(frame_id - FrameId::first()) % + pending_frames_.size()]; +} + +void Receiver::RecordNewTargetPlayoutDelay(FrameId as_of_frame, + milliseconds delay) { + OSP_DCHECK_GT(as_of_frame, checkpoint_frame()); + + // Prune-out entries from |playout_delay_changes_| that are no longer needed. + // At least one entry must always be kept (i.e., there must always be a + // "current" setting). + const FrameId next_frame = last_frame_consumed_ + 1; + const auto keep_one_before_it = std::find_if( + std::next(playout_delay_changes_.begin()), playout_delay_changes_.end(), + [&](const auto& entry) { return entry.first > next_frame; }); + playout_delay_changes_.erase(playout_delay_changes_.begin(), + std::prev(keep_one_before_it)); + + // Insert the delay change entry, maintaining the ascending ordering of the + // vector. + const auto insert_it = std::find_if( + playout_delay_changes_.begin(), playout_delay_changes_.end(), + [&](const auto& entry) { return entry.first > as_of_frame; }); + playout_delay_changes_.emplace(insert_it, as_of_frame, delay); + + OSP_DCHECK(openscreen::AreElementsSortedAndUnique(playout_delay_changes_)); +} + +milliseconds Receiver::ResolveTargetPlayoutDelay(FrameId frame_id) const { + OSP_DCHECK_GT(frame_id, last_frame_consumed_); + +#if OSP_DCHECK_IS_ON() + // Extra precaution: Ensure all possible playout delay changes are known. In + // other words, every unconsumed frame in the queue, up to (and including) + // |frame_id|, must have an assigned estimated_capture_time. + for (FrameId f = last_frame_consumed_ + 1; f <= frame_id; ++f) { + OSP_DCHECK(GetQueueEntry(f).estimated_capture_time) + << " don't know whether there was a playout delay change for frame " + << f; + } +#endif + + const auto it = std::find_if( + playout_delay_changes_.crbegin(), playout_delay_changes_.crend(), + [&](const auto& entry) { return entry.first <= frame_id; }); + OSP_DCHECK(it != playout_delay_changes_.crend()); + return it->second; +} + +void Receiver::AdvanceCheckpoint(FrameId new_checkpoint) { + OSP_DCHECK_GT(new_checkpoint, checkpoint_frame()); + OSP_DCHECK_LE(new_checkpoint, latest_frame_expected_); + + while (new_checkpoint < latest_frame_expected_) { + const FrameId next = new_checkpoint + 1; + if (!GetQueueEntry(next).collector.is_complete()) { + break; + } + new_checkpoint = next; + } + + RECEIVER_VLOG << "Advancing checkpoint to " << new_checkpoint; + set_checkpoint_frame(new_checkpoint); + rtcp_builder_.SetPlayoutDelay(ResolveTargetPlayoutDelay(new_checkpoint)); + SendRtcp(); +} + +void Receiver::DropAllFramesBefore(FrameId first_kept_frame) { + // The following DCHECKs are verifying that this method is only being called + // because one or more incomplete frames are being skipped-over. + const FrameId first_to_drop = last_frame_consumed_ + 1; + OSP_DCHECK_GT(first_kept_frame, first_to_drop); + OSP_DCHECK_GT(first_kept_frame, checkpoint_frame()); + OSP_DCHECK_LE(first_kept_frame, latest_frame_expected_); + + // Reset each of the frames being dropped, pretending that they were consumed. + for (FrameId f = first_to_drop; f < first_kept_frame; ++f) { + PendingFrame& entry = GetQueueEntry(f); + // Pedantic sanity-check: Ensure the "target playout delay change" data + // dependency was satisfied. See comments in AdvanceToNextFrame(). + OSP_DCHECK(entry.estimated_capture_time); + entry.Reset(); + } + last_frame_consumed_ = first_kept_frame - 1; + + RECEIVER_LOG(INFO) << "Artificially advancing checkpoint after skipping."; + AdvanceCheckpoint(first_kept_frame); +} + +void Receiver::ScheduleFrameReadyCheck(Clock::time_point when) { + consumption_alarm_.Schedule( + [this] { + if (consumer_) { + const int next_frame_buffer_size = AdvanceToNextFrame(); + if (next_frame_buffer_size != kNoFramesReady) { + consumer_->OnFramesReady(next_frame_buffer_size); + } + } + }, + when); +} + +Receiver::Consumer::~Consumer() = default; + +Receiver::PendingFrame::PendingFrame() = default; +Receiver::PendingFrame::~PendingFrame() = default; + +void Receiver::PendingFrame::Reset() { + collector.Reset(); + estimated_capture_time = absl::nullopt; +} + +// static +constexpr milliseconds Receiver::kDefaultPlayerProcessingTime; +constexpr int Receiver::kNoFramesReady; +constexpr milliseconds Receiver::kNackFeedbackInterval; + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/receiver.h b/cast/streaming/receiver.h new file mode 100644 index 00000000..d1ea8b0c --- /dev/null +++ b/cast/streaming/receiver.h @@ -0,0 +1,351 @@ +// 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. + +#ifndef CAST_STREAMING_RECEIVER_H_ +#define CAST_STREAMING_RECEIVER_H_ + +#include <stdint.h> + +#include <array> +#include <chrono> // NOLINT +#include <memory> +#include <vector> + +#include "absl/types/optional.h" +#include "absl/types/span.h" +#include "cast/streaming/clock_drift_smoother.h" +#include "cast/streaming/compound_rtcp_builder.h" +#include "cast/streaming/environment.h" +#include "cast/streaming/frame_collector.h" +#include "cast/streaming/frame_id.h" +#include "cast/streaming/packet_receive_stats_tracker.h" +#include "cast/streaming/rtcp_common.h" +#include "cast/streaming/rtcp_session.h" +#include "cast/streaming/rtp_packet_parser.h" +#include "cast/streaming/sender_report_parser.h" +#include "cast/streaming/session_config.h" +#include "cast/streaming/ssrc.h" +#include "platform/api/time.h" +#include "util/alarm.h" + +namespace cast { +namespace streaming { + +struct EncodedFrame; +class ReceiverPacketRouter; + +// The Cast Streaming Receiver, a peer corresponding to some Cast Streaming +// Sender at the other end of a network link. +// +// Cast Streaming is a transport protocol which divides up the frames for one +// media stream (e.g., audio or video) into multiple RTP packets containing an +// encrypted payload. The Receiver is the peer responsible for collecting the +// RTP packets, decrypting the payload, and re-assembling a frame that can be +// passed to a decoder and played out. +// +// A Sender ↔ Receiver pair is used to transport each media stream. Typically, +// there are two pairs in a normal system, one for the audio stream and one for +// video stream. A local player is responsible for synchronizing the playout of +// the frames of each stream to achieve lip-sync. See the discussion in +// encoded_frame.h for how the |reference_time| and |rtp_timestamp| of the +// EncodedFrames are used to achieve this. +// +// See the Receiver Demo app for a reference implementation that both shows and +// explains how Receivers are properly configured and started, integrated with a +// decoder, and the resulting decoded media is played out. Also, here is a +// general usage example: +// +// class MyPlayer : public cast::streaming::Receiver::Consumer { +// public: +// explicit MyPlayer(Receiver* receiver) : receiver_(receiver) { +// recevier_->SetPlayerProcessingTime(std::chrono::milliseconds(10)); +// receiver_->SetConsumer(this); +// } +// +// ~MyPlayer() override { +// receiver_->SetConsumer(nullptr); +// } +// +// private: +// // Receiver::Consumer implementation. +// void OnFramesReady(int next_frame_buffer_size) override { +// std::vector<uint8_t> buffer; +// buffer.resize(next_frame_buffer_size); +// cast::streaming::EncodedFrame encoded_frame = +// receiver_->ConsumeNextFrame(absl::Span<uint8_t>(buffer)); +// +// display_.RenderFrame(decoder_.DecodeFrame(encoded_frame.data)); +// +// // Note: An implementation could call receiver_->AdvanceToNextFrame() +// // and receiver_->ConsumeNextFrame() in a loop here, to consume all the +// // remaining frames that are ready. +// } +// +// Receiver* const receiver_; +// MyDecoder decoder_; +// MyDisplay display_; +// }; +// +// Internally, a queue of complete and partially-received frames is maintained. +// The queue is a circular queue of FrameCollectors that each maintain the +// individual receive state of each in-flight frame. There are three conceptual +// "pointers" that indicate what assumptions and operations are made on certain +// ranges of frames in the queue: +// +// 1. Latest Frame Expected: The FrameId of the latest frame whose existence +// is known to this Receiver. This is the highest FrameId seen in any +// successfully-parsed RTP packet. +// 2. Checkpoint Frame: Indicates that all of the RTP packets for all frames +// up to and including the one having this FrameId have been successfully +// received and processed. +// 3. Last Frame Consumed: The FrameId of last frame consumed (see +// ConsumeNextFrame()). Once a frame is consumed, all internal resources +// related to the frame can be freed and/or re-used for later frames. +class Receiver { + public: + class Consumer { + public: + virtual ~Consumer(); + + // Called whenever one or more frames have become ready for consumption. The + // |next_frame_buffer_size| argument is identical to the result of calling + // AdvanceToNextFrame(), and so the Consumer only needs to prepare a buffer + // and call ConsumeNextFrame(). It may then call AdvanceToNextFrame() to + // check whether there are any more frames ready, but this is not mandatory. + // See usage example in class-level comments. + virtual void OnFramesReady(int next_frame_buffer_size) = 0; + }; + + // Constructs a Receiver that attaches to the given |environment| and + // |packet_router|. The config contains the settings that were + // agreed-upon by both sides from the OFFER/ANSWER exchange (i.e., the part of + // the overall end-to-end connection process that occurs before Cast Streaming + // is started). + Receiver(Environment* environment, + ReceiverPacketRouter* packet_router, + const cast::streaming::SessionConfig& config, + std::chrono::milliseconds initial_target_playout_delay); + ~Receiver(); + + Ssrc ssrc() const { return rtcp_session_.receiver_ssrc(); } + int rtp_timebase() const { return rtp_timebase_; } + + // Set the Consumer receiving notifications when new frames are ready for + // consumption. Frames received before this method is called will remain in + // the queue indefinitely. + void SetConsumer(Consumer* consumer); + + // Sets how much time the consumer will need to decode/buffer/render/etc., and + // otherwise fully process a frame for on-time playback. This information is + // used by the Receiver to decide whether to skip past frames that have + // arrived too late. This method can be called repeatedly to make adjustments + // based on changing environmental conditions. + // + // Default setting: kDefaultPlayerProcessingTime + void SetPlayerProcessingTime( + openscreen::platform::Clock::duration needed_time); + + // Propagates a "picture loss indicator" notification to the Sender, + // requesting a key frame so that decode/playout can recover. It is safe to + // call this redundantly. The Receiver will clear the picture loss condition + // automatically, once a key frame is received (i.e., before + // ConsumeNextFrame() is called to access it). + void RequestKeyFrame(); + + // Advances to the next frame ready for consumption. This may skip-over + // incomplete frames that will not play out on-time; but only if there are + // completed frames further down the queue that have no dependency + // relationship with them (e.g., key frames). + // + // This method returns kNoFramesReady if there is not currently a frame ready + // for consumption. The caller should wait for a Consumer::OnFramesReady() + // notification before trying again. Otherwise, the number of bytes of encoded + // data is returned, and the caller should use this to ensure the buffer it + // passes to ConsumeNextFrame() is large enough. + int AdvanceToNextFrame(); + + // Returns the next frame, both metadata and payload data. The Consumer calls + // this method after being notified via OnFramesReady(), and it can also call + // this whenever AdvanceToNextFrame() indicates another frame is ready. + // |buffer| must point to a sufficiently-sized buffer that will be populated + // with the frame's payload data. Upon return |frame->data| will be set to the + // portion of the buffer that was populated. + EncodedFrame ConsumeNextFrame(absl::Span<uint8_t> buffer); + + // The default "player processing time" amount. See SetPlayerProcessingTime(). + static constexpr std::chrono::milliseconds kDefaultPlayerProcessingTime{5}; + + // Returned by AdvanceToNextFrame() when there are no frames currently ready + // for consumption. + static constexpr int kNoFramesReady = -1; + + protected: + friend class ReceiverPacketRouter; + + // Called by ReceiverPacketRouter to provide this Receiver with what looks + // like a RTP/RTCP packet meant for it specifically (among other Receivers). + void OnReceivedRtpPacket(openscreen::platform::Clock::time_point arrival_time, + std::vector<uint8_t> packet); + void OnReceivedRtcpPacket( + openscreen::platform::Clock::time_point arrival_time, + std::vector<uint8_t> packet); + + private: + // An entry in the circular queue (see |pending_frames_|). + struct PendingFrame { + FrameCollector collector; + + // The Receiver's [local] Clock time when this frame was originally captured + // at the Sender. This is computed and assigned when the RTP packet with ID + // 0 is processed. Add the target playout delay to this to get the target + // playout time. + absl::optional<openscreen::platform::Clock::time_point> + estimated_capture_time; + + PendingFrame(); + ~PendingFrame(); + + // Reset this entry to its initial state, freeing resources. + void Reset(); + }; + + // Get/Set the checkpoint FrameId. This indicates that all of the packets for + // all frames up to and including this FrameId have been successfully received + // (or otherwise do not need to be re-transmitted). + FrameId checkpoint_frame() const { return rtcp_builder_.checkpoint_frame(); } + void set_checkpoint_frame(FrameId frame_id) { + rtcp_builder_.SetCheckpointFrame(frame_id); + } + + // Send an RTCP packet to the Sender immediately, to acknowledge the complete + // reception of one or more additional frames, to reply to a Sender Report, or + // to request re-transmits. Calling this also schedules additional RTCP + // packets to be sent periodically for the life of this Receiver. + void SendRtcp(); + + // Helpers to map the given |frame_id| to the element in the |pending_frames_| + // circular queue. There are both const and non-const versions, but neither + // mutate any state (i.e., they are just look-ups). + const PendingFrame& GetQueueEntry(FrameId frame_id) const; + PendingFrame& GetQueueEntry(FrameId frame_id); + + // Record that the target playout delay has changed starting with the given + // FrameId. + void RecordNewTargetPlayoutDelay(FrameId as_of_frame, + std::chrono::milliseconds delay); + + // Examine the known target playout delay changes to determine what setting is + // in-effect for the given frame. + std::chrono::milliseconds ResolveTargetPlayoutDelay(FrameId frame_id) const; + + // Called to move the checkpoint forward. This scans the queue, starting from + // |new_checkpoint|, to find the latest in a contiguous sequence of completed + // frames. Then, it records that frame as the new checkpoint, and immediately + // sends a feedback RTCP packet to the Sender. + void AdvanceCheckpoint(FrameId new_checkpoint); + + // Helper to force-drop all frames before |first_kept_frame|, even if they + // were never consumed. This will also auto-cancel frames that were never + // completely received, artificially moving the checkpoint forward, and + // notifying the Sender of that. The caller of this method is responsible for + // making sure that frame data dependencies will not be broken by dropping the + // frames. + void DropAllFramesBefore(FrameId first_kept_frame); + + // Sets the |consumption_alarm_| to check whether any frames are ready, + // including possibly skipping over late frames in order to make not-yet-late + // frames become ready. The default argument value means "without delay." + void ScheduleFrameReadyCheck( + openscreen::platform::Clock::time_point when = {}); + + const openscreen::platform::ClockNowFunctionPtr now_; + ReceiverPacketRouter* const packet_router_; + RtcpSession rtcp_session_; + SenderReportParser rtcp_parser_; + CompoundRtcpBuilder rtcp_builder_; + PacketReceiveStatsTracker stats_tracker_; // Tracks transmission stats. + RtpPacketParser rtp_parser_; + const int rtp_timebase_; // RTP timestamp ticks per second. + const FrameCrypto crypto_; // Decrypts assembled frames. + + // Buffer for serializing/sending RTCP packets. + const int rtcp_buffer_capacity_; + const std::unique_ptr<uint8_t[]> rtcp_buffer_; + + // Schedules tasks to ensure RTCP reports are sent within a bounded interval. + // Not scheduled until after this Receiver has processed the first packet from + // the Sender. + openscreen::Alarm rtcp_alarm_; + openscreen::platform::Clock::time_point last_rtcp_send_time_ = + openscreen::platform::Clock::time_point::min(); + + // The last Sender Report received and when the packet containing it had + // arrived. This contains lip-sync timestamps used as part of the calculation + // of playout times for the received frames, as well as ping-pong data bounced + // back to the Sender in the Receiver Reports. It is nullopt until the first + // parseable Sender Report is received. + absl::optional<SenderReportParser::SenderReportWithId> last_sender_report_; + openscreen::platform::Clock::time_point last_sender_report_arrival_time_; + + // Tracks the offset between the Receiver's [local] clock and the Sender's + // clock. This is invalid until the first Sender Report has been successfully + // processed (i.e., |last_sender_report_| is not nullopt). + ClockDriftSmoother smoothed_clock_offset_; + + // The ID of the latest frame whose existence is known to this Receiver. This + // value must always be greater than or equal to |checkpoint_frame()|. + FrameId latest_frame_expected_ = FrameId::first() - 1; + + // The ID of the last frame consumed. This value must always be less than or + // equal to |checkpoint_frame()|, since it's impossible to consume incomplete + // frames! + FrameId last_frame_consumed_ = FrameId::first() - 1; + + // The ID of the latest key frame known to be in-flight. This is used by + // RequestKeyFrame() to ensure the PLI condition doesn't get set again until + // after the consumer has seen a key frame that would clear the condition. + FrameId last_key_frame_received_; + + // The frame queue (circular), which tracks which frames are in-flight, stores + // data for partially-received frames, and holds onto completed frames until + // the consumer consumes them. + // + // Use GetQueueEntry() to access a slot. The currently-active slots are those + // for the frames after |last_frame_consumed_| and up-to/including + // |latest_frame_expected_|. + std::array<PendingFrame, kMaxUnackedFrames> pending_frames_{}; + + // Tracks the recent changes to the target playout delay, which is controlled + // by the Sender. The FrameId indicates the first frame where a new delay + // setting takes effect. This vector is never empty, is kept sorted, and is + // pruned to remain as small as possible. + // + // The target playout delay is the amount of time between a frame's + // capture/recording on the Sender and when it should be played-out at the + // Receiver. + std::vector<std::pair<FrameId, std::chrono::milliseconds>> + playout_delay_changes_; + + // The consumer to notify when there are one or more frames completed and + // ready to be consumed. + Consumer* consumer_ = nullptr; + + // The additional time needed to decode/play-out each frame after being + // consumed from this Receiver. + openscreen::platform::Clock::duration player_processing_time_ = + kDefaultPlayerProcessingTime; + + // Scheduled to check whether there are frames ready and, if there are, to + // notify the Consumer via OnFramesReady(). + openscreen::Alarm consumption_alarm_; + + // The interval between sending ACK/NACK feedback RTCP messages while + // incomplete frames exist in the queue. + static constexpr std::chrono::milliseconds kNackFeedbackInterval{30}; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_RECEIVER_H_ diff --git a/cast/streaming/receiver_packet_router.cc b/cast/streaming/receiver_packet_router.cc new file mode 100644 index 00000000..5e3d17bd --- /dev/null +++ b/cast/streaming/receiver_packet_router.cc @@ -0,0 +1,119 @@ +// 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/receiver_packet_router.h" + +#include <algorithm> +#include <iomanip> + +#include "cast/streaming/packet_util.h" +#include "cast/streaming/receiver.h" +#include "util/logging.h" + +using openscreen::IPEndpoint; +using openscreen::platform::Clock; + +namespace cast { +namespace streaming { + +ReceiverPacketRouter::ReceiverPacketRouter(Environment* environment) + : environment_(environment) { + OSP_DCHECK(environment_); +} + +ReceiverPacketRouter::~ReceiverPacketRouter() { + OSP_DCHECK(receivers_.empty()); +} + +void ReceiverPacketRouter::OnReceiverCreated(Ssrc sender_ssrc, + Receiver* receiver) { + OSP_DCHECK(FindEntry(sender_ssrc) == receivers_.end()); + receivers_.emplace_back(sender_ssrc, receiver); + + // If there were no Receiver instances before, resume receiving packets for + // dispatch. Reset/Clear the remote endpoint, in preparation for later setting + // it to the source of the first packet received. + if (receivers_.size() == 1) { + environment_->set_remote_endpoint(IPEndpoint{}); + environment_->ConsumeIncomingPackets(this); + } +} + +void ReceiverPacketRouter::OnReceiverDestroyed(Ssrc sender_ssrc) { + const auto it = FindEntry(sender_ssrc); + OSP_DCHECK(it != receivers_.end()); + receivers_.erase(it); + + // If there are no longer any Receivers, suspend receiving packets. + if (receivers_.empty()) { + environment_->DropIncomingPackets(); + } +} + +void ReceiverPacketRouter::SendRtcpPacket(absl::Span<const uint8_t> packet) { + OSP_DCHECK(InspectPacketForRouting(packet).first == ApparentPacketType::RTCP); + + // Do not proceed until the remote endpoint is known. See OnReceivedPacket(). + if (environment_->remote_endpoint().port == 0) { + return; + } + + environment_->SendPacket(packet); +} + +void ReceiverPacketRouter::OnReceivedPacket(const IPEndpoint& source, + Clock::time_point arrival_time, + std::vector<uint8_t> packet) { + OSP_DCHECK_NE(source.port, uint16_t{0}); + + // If the sender endpoint is known, ignore any packet that did not come from + // that same endpoint. + if (environment_->remote_endpoint().port != 0) { + if (source != environment_->remote_endpoint()) { + return; + } + } + + const std::pair<ApparentPacketType, Ssrc> seems_like = + InspectPacketForRouting(packet); + if (seems_like.first == ApparentPacketType::UNKNOWN) { + // If the packet type is unknown, log a warning containing a hex dump. + constexpr int kMaxDumpSize = 96; + std::ostringstream hex_dump; + hex_dump << std::setfill('0') << std::hex; + for (int i = 0, len = std::min<int>(packet.size(), kMaxDumpSize); i < len; + ++i) { + hex_dump << std::setw(2) << static_cast<int>(packet[i]); + } + OSP_LOG_WARN << "UNKNOWN packet of " << packet.size() + << " bytes. Partial hex dump: " << hex_dump.str(); + return; + } + const auto it = FindEntry(seems_like.second); + if (it != receivers_.end()) { + // At this point, a valid packet has been matched with a receiver. Lock-in + // the remote endpoint as the |source| of this |packet| so that only packets + // from the same source are permitted from here onwards. + if (environment_->remote_endpoint().port == 0) { + environment_->set_remote_endpoint(source); + } + + if (seems_like.first == ApparentPacketType::RTP) { + it->second->OnReceivedRtpPacket(arrival_time, std::move(packet)); + } else if (seems_like.first == ApparentPacketType::RTCP) { + it->second->OnReceivedRtcpPacket(arrival_time, std::move(packet)); + } + } +} + +ReceiverPacketRouter::ReceiverEntries::iterator ReceiverPacketRouter::FindEntry( + Ssrc sender_ssrc) { + return std::find_if(receivers_.begin(), receivers_.end(), + [sender_ssrc](const ReceiverEntries::value_type& entry) { + return entry.first == sender_ssrc; + }); +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/receiver_packet_router.h b/cast/streaming/receiver_packet_router.h new file mode 100644 index 00000000..88db08a8 --- /dev/null +++ b/cast/streaming/receiver_packet_router.h @@ -0,0 +1,66 @@ +// 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. + +#ifndef CAST_STREAMING_RECEIVER_PACKET_ROUTER_H_ +#define CAST_STREAMING_RECEIVER_PACKET_ROUTER_H_ + +#include <stdint.h> + +#include <utility> +#include <vector> + +#include "absl/types/span.h" +#include "cast/streaming/environment.h" +#include "cast/streaming/ssrc.h" + +namespace cast { +namespace streaming { + +class Receiver; + +// Handles all network I/O among multiple Receivers meant for synchronized +// play-out (e.g., one Receiver for audio, one Receiver for video). Incoming +// traffic is dispatched to the appropriate Receiver, based on its corresponding +// sender's SSRC. Also, all traffic not coming from the same source is +// filtered-out. +class ReceiverPacketRouter final : public Environment::PacketConsumer { + public: + explicit ReceiverPacketRouter(Environment* environment); + ~ReceiverPacketRouter() final; + + protected: + friend class Receiver; + + // Called from a Receiver constructor/destructor to register/deregister a + // Receiver instance that processes RTP/RTCP packets from a Sender having the + // given SSRC. + void OnReceiverCreated(Ssrc sender_ssrc, Receiver* receiver); + void OnReceiverDestroyed(Ssrc sender_ssrc); + + // Called by a Receiver to send a RTCP packet back to the source from which + // earlier packets were received, or does nothing if OnReceivedPacket() has + // not been called yet. + void SendRtcpPacket(absl::Span<const uint8_t> packet); + + private: + using ReceiverEntries = std::vector<std::pair<Ssrc, Receiver*>>; + + // Environment::PacketConsumer implementation. + void OnReceivedPacket(const openscreen::IPEndpoint& source, + openscreen::platform::Clock::time_point arrival_time, + std::vector<uint8_t> packet) final; + + // Helper to return an iterator pointing to the entry corresponding to the + // given |sender_ssrc|, or "end" if not found. + ReceiverEntries::iterator FindEntry(Ssrc sender_ssrc); + + Environment* const environment_; + + ReceiverEntries receivers_; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_RECEIVER_PACKET_ROUTER_H_ diff --git a/cast/streaming/receiver_session.h b/cast/streaming/receiver_session.h index 8a60ec56..248bf8ad 100644 --- a/cast/streaming/receiver_session.h +++ b/cast/streaming/receiver_session.h @@ -8,17 +8,13 @@ #include <memory> #include <vector> +#include "cast/streaming/receiver.h" +#include "cast/streaming/receiver_packet_router.h" #include "cast/streaming/session_config.h" -#include "streaming/cast/receiver.h" -#include "streaming/cast/receiver_packet_router.h" namespace cast { namespace streaming { -// TODO(jophba): remove once these namespaces are merged. -using openscreen::cast_streaming::Receiver; -using openscreen::cast_streaming::ReceiverPacketRouter; - class ReceiverSession { public: class ConfiguredReceivers { diff --git a/cast/streaming/receiver_unittest.cc b/cast/streaming/receiver_unittest.cc new file mode 100644 index 00000000..2426376a --- /dev/null +++ b/cast/streaming/receiver_unittest.cc @@ -0,0 +1,843 @@ +// 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/receiver.h" + +#include <stdint.h> + +#include <algorithm> +#include <array> +#include <vector> + +#include "absl/types/span.h" +#include "cast/streaming/compound_rtcp_parser.h" +#include "cast/streaming/constants.h" +#include "cast/streaming/encoded_frame.h" +#include "cast/streaming/frame_crypto.h" +#include "cast/streaming/receiver_packet_router.h" +#include "cast/streaming/rtcp_common.h" +#include "cast/streaming/rtcp_session.h" +#include "cast/streaming/rtp_defines.h" +#include "cast/streaming/rtp_packetizer.h" +#include "cast/streaming/rtp_time.h" +#include "cast/streaming/sender_report_builder.h" +#include "cast/streaming/session_config.h" +#include "cast/streaming/ssrc.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "platform/api/time.h" +#include "platform/api/udp_socket.h" +#include "platform/base/error.h" +#include "platform/base/ip_address.h" +#include "platform/base/udp_packet.h" +#include "platform/test/fake_clock.h" +#include "platform/test/fake_task_runner.h" +#include "util/logging.h" + +using openscreen::Error; +using openscreen::ErrorOr; +using openscreen::IPAddress; +using openscreen::IPEndpoint; +using openscreen::platform::Clock; +using openscreen::platform::ClockNowFunctionPtr; +using openscreen::platform::FakeClock; +using openscreen::platform::FakeTaskRunner; +using openscreen::platform::TaskRunner; +using openscreen::platform::UdpPacket; +using openscreen::platform::UdpSocket; + +using std::chrono::duration_cast; +using std::chrono::microseconds; +using std::chrono::milliseconds; +using std::chrono::seconds; + +using testing::_; +using testing::AtLeast; +using testing::Gt; +using testing::Invoke; +using testing::SaveArg; + +namespace cast { +namespace streaming { +namespace { + +// Receiver configuration. + +constexpr Ssrc kSenderSsrc = 1; +constexpr Ssrc kReceiverSsrc = 2; +constexpr int kRtpTimebase = 48000; +constexpr milliseconds kTargetPlayoutDelay{100}; +constexpr auto kAesKey = + std::array<uint8_t, 16>{{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}}; +constexpr auto kCastIvMask = + std::array<uint8_t, 16>{{0xf0, 0xe0, 0xd0, 0xc0, 0xb0, 0xa0, 0x90, 0x80, + 0x70, 0x60, 0x50, 0x40, 0x30, 0x20, 0x10, 0x00}}; + +constexpr milliseconds kTargetPlayoutDelayChange{800}; +// Additional configuration for the Sender. +constexpr RtpPayloadType kRtpPayloadType = RtpPayloadType::kVideoVp8; +constexpr int kMaxRtpPacketSize = 64; + +// A simulated one-way network delay. +constexpr auto kOneWayNetworkDelay = milliseconds(23); + +// An EncodedFrame for unit testing, one of a sequence of simulated frames, each +// of 10 ms duration. The first frame will be a key frame; and any later frames +// will be non-key, dependent on the prior frame. Frame 5 (the 6th frame in the +// zero-based sequence) will include a target playout delay change, an increase +// to 800 ms. Frames with different IDs will contain vary in their payload data +// size, but are always 3 or more packets' worth of data. +struct SimulatedFrame : public EncodedFrame { + static constexpr milliseconds kFrameDuration = milliseconds(10); + static constexpr milliseconds kTargetPlayoutDelayChange = milliseconds(800); + + static constexpr int kPlayoutChangeAtFrame = 5; + + SimulatedFrame(Clock::time_point first_frame_reference_time, int which) { + frame_id = FrameId::first() + which; + if (which == 0) { + dependency = EncodedFrame::KEY_FRAME; + referenced_frame_id = frame_id; + } else { + dependency = EncodedFrame::DEPENDS_ON_ANOTHER; + referenced_frame_id = frame_id - 1; + } + rtp_timestamp = + GetRtpStartTime() + + RtpTimeDelta::FromDuration(kFrameDuration * which, kRtpTimebase); + reference_time = first_frame_reference_time + kFrameDuration * which; + if (which == kPlayoutChangeAtFrame) { + new_playout_delay = kTargetPlayoutDelayChange; + } + constexpr int kAdditionalBytesEachSuccessiveFrame = 3; + buffer_.resize(3 * kMaxRtpPacketSize + + which * kAdditionalBytesEachSuccessiveFrame); + for (size_t i = 0; i < buffer_.size(); ++i) { + buffer_[i] = static_cast<uint8_t>(which + static_cast<int>(i)); + } + data = absl::Span<uint8_t>(buffer_); + } + + static RtpTimeTicks GetRtpStartTime() { + return RtpTimeTicks::FromTimeSinceOrigin(seconds(0), kRtpTimebase); + } + + static milliseconds GetExpectedPlayoutDelay(int which) { + return (which < kPlayoutChangeAtFrame) ? kTargetPlayoutDelay + : kTargetPlayoutDelayChange; + } + + private: + std::vector<uint8_t> buffer_; +}; + +// static +constexpr milliseconds SimulatedFrame::kFrameDuration; +constexpr milliseconds SimulatedFrame::kTargetPlayoutDelayChange; +constexpr int SimulatedFrame::kPlayoutChangeAtFrame; + +// Processes packets from the Receiver under test, as a real Sender might, and +// allows the unit tests to set expectations on events of interest to confirm +// proper behavior of the Receiver. +class MockSender : public CompoundRtcpParser::Client { + public: + MockSender(TaskRunner* task_runner, UdpSocket::Client* receiver) + : task_runner_(task_runner), + receiver_(receiver), + sender_endpoint_{ + // Use a random IPv6 address in the range reserved for + // "documentation purposes." Thus, the following is a fake address + // that should be blocked by the OS (and all network packet + // routers). But, these tests don't use real sockets, so... + IPAddress::Parse("2001:db8:0d93:69c2:fd1a:49a6:a7c0:e8a6").value(), + 2344}, + rtcp_session_(kSenderSsrc, kReceiverSsrc, FakeClock::now()), + sender_report_builder_(&rtcp_session_), + rtcp_parser_(&rtcp_session_, this), + crypto_(kAesKey, kCastIvMask), + rtp_packetizer_(kRtpPayloadType, kSenderSsrc, kMaxRtpPacketSize) {} + + ~MockSender() override = default; + + void set_max_feedback_frame_id(FrameId f) { max_feedback_frame_id_ = f; } + + // Called by the test procedures to generate a Sender Report containing the + // given lip-sync timestamps, and send it to the Receiver. The caller must + // spin the TaskRunner for the RTCP packet to be delivered to the Receiver. + StatusReportId SendSenderReport(Clock::time_point reference_time, + RtpTimeTicks rtp_timestamp) { + // Generate the Sender Report RTCP packet. + uint8_t buffer[kMaxRtpPacketSizeForIpv4UdpOnEthernet]; + RtcpSenderReport sender_report; + sender_report.reference_time = reference_time; + sender_report.rtp_timestamp = rtp_timestamp; + const auto packet_and_report_id = + sender_report_builder_.BuildPacket(sender_report, buffer); + + // Send the RTCP packet as a UdpPacket directly to the Receiver instance. + UdpPacket packet_to_send(packet_and_report_id.first.begin(), + packet_and_report_id.first.end()); + packet_to_send.set_source(sender_endpoint_); + task_runner_->PostTask( + [receiver = receiver_, packet = std::move(packet_to_send)]() mutable { + receiver->OnRead(nullptr, ErrorOr<UdpPacket>(std::move(packet))); + }); + + return packet_and_report_id.second; + } + + // Sets which frame is currently being sent by this MockSender. Test code must + // call SendRtpPackets() to send the packets. + void SetFrameBeingSent(const EncodedFrame& frame) { + frame_being_sent_ = crypto_.Encrypt(frame); + } + + // Returns a vector containing each packet ID once (of the current frame being + // sent). |permutation| controls the sort order of the vector: zero will + // provide all the packet IDs in order, and greater values will provide them + // in a different, predictable order. + std::vector<FramePacketId> GetAllPacketIds(int permutation) { + const int num_packets = + rtp_packetizer_.ComputeNumberOfPackets(frame_being_sent_); + OSP_CHECK_GT(num_packets, 0); + std::vector<FramePacketId> ids; + ids.reserve(num_packets); + const FramePacketId last_packet_id = + static_cast<FramePacketId>(num_packets - 1); + for (FramePacketId packet_id = 0; packet_id <= last_packet_id; + ++packet_id) { + ids.push_back(packet_id); + } + for (int i = 0; i < permutation; ++i) { + std::next_permutation(ids.begin(), ids.end()); + } + return ids; + } + + // Send the specified packets of the current frame being sent. + void SendRtpPackets(const std::vector<FramePacketId>& packets_to_send) { + uint8_t buffer[kMaxRtpPacketSize]; + for (FramePacketId packet_id : packets_to_send) { + const auto span = + rtp_packetizer_.GeneratePacket(frame_being_sent_, packet_id, buffer); + UdpPacket packet_to_send(span.begin(), span.end()); + packet_to_send.set_source(sender_endpoint_); + task_runner_->PostTask( + [receiver = receiver_, packet = std::move(packet_to_send)]() mutable { + receiver->OnRead(nullptr, ErrorOr<UdpPacket>(std::move(packet))); + }); + } + } + + // Called to process a packet from the Receiver. + void OnPacketFromReceiver(absl::Span<const uint8_t> packet) { + EXPECT_TRUE(rtcp_parser_.Parse(packet, max_feedback_frame_id_)); + } + + // CompoundRtcpParser::Client implementation: Tests set expectations on these + // mocks to confirm that the receiver is providing the right data to the + // sender in its RTCP packets. + MOCK_METHOD1(OnReceiverReferenceTimeAdvanced, + void(Clock::time_point reference_time)); + MOCK_METHOD1(OnReceiverReport, void(const RtcpReportBlock& receiver_report)); + MOCK_METHOD0(OnReceiverIndicatesPictureLoss, void()); + MOCK_METHOD2(OnReceiverCheckpoint, + void(FrameId frame_id, milliseconds playout_delay)); + MOCK_METHOD1(OnReceiverHasFrames, void(std::vector<FrameId> acks)); + MOCK_METHOD1(OnReceiverIsMissingPackets, void(std::vector<PacketNack> nacks)); + + private: + TaskRunner* const task_runner_; + UdpSocket::Client* const receiver_; + const IPEndpoint sender_endpoint_; + RtcpSession rtcp_session_; + SenderReportBuilder sender_report_builder_; + CompoundRtcpParser rtcp_parser_; + FrameCrypto crypto_; + RtpPacketizer rtp_packetizer_; + + FrameId max_feedback_frame_id_ = FrameId::first() + kMaxUnackedFrames; + + EncryptedFrame frame_being_sent_; +}; + +// An Environment that can intercept all packet sends. ReceiverTest will connect +// the SendPacket() method calls to the MockSender. +class MockEnvironment : public Environment { + public: + MockEnvironment(ClockNowFunctionPtr now_function, TaskRunner* task_runner) + : Environment(now_function, task_runner) {} + + ~MockEnvironment() override = default; + + // Used for intercepting packet sends from the implementation under test. + MOCK_METHOD1(SendPacket, void(absl::Span<const uint8_t> packet)); +}; + +class MockConsumer : public Receiver::Consumer { + public: + MOCK_METHOD1(OnFramesReady, void(int next_frame_buffer_size)); +}; + +class ReceiverTest : public testing::Test { + public: + ReceiverTest() + : clock_(Clock::now()), + task_runner_(&clock_), + env_(&FakeClock::now, &task_runner_), + packet_router_(&env_), + receiver_(&env_, + &packet_router_, + {/* .sender_ssrc = */ kSenderSsrc, + /* .receiver_ssrc = */ kReceiverSsrc, + /* .rtp_timebase = */ kRtpTimebase, + /* .channels = */ 2, + /* .aes_secret_key = */ kAesKey, + /* .aes_iv_mask = */ kCastIvMask}, + kTargetPlayoutDelay), + sender_(&task_runner_, &env_) { + env_.set_socket_error_handler( + [](Error error) { ASSERT_TRUE(error.ok()) << error; }); + ON_CALL(env_, SendPacket(_)) + .WillByDefault(Invoke(&sender_, &MockSender::OnPacketFromReceiver)); + receiver_.SetConsumer(&consumer_); + } + + ~ReceiverTest() override = default; + + Receiver* receiver() { return &receiver_; } + MockSender* sender() { return &sender_; } + MockConsumer* consumer() { return &consumer_; } + + void AdvanceClockAndRunTasks(Clock::duration delta) { clock_.Advance(delta); } + void RunTasksUntilIdle() { task_runner_.RunTasksUntilIdle(); } + + // Consume one frame from the Receiver, and verify that it is the same as the + // |sent_frame|. Exception: The |reference_time| is the playout time on the + // Receiver's end, while it refers to the capture time on the Sender's end. + void ConsumeAndVerifyFrame(const SimulatedFrame& sent_frame) { + SCOPED_TRACE(testing::Message() << "for frame " << sent_frame.frame_id); + + const int payload_size = receiver()->AdvanceToNextFrame(); + ASSERT_NE(Receiver::kNoFramesReady, payload_size); + std::vector<uint8_t> buffer(payload_size); + EncodedFrame received_frame = + receiver()->ConsumeNextFrame(absl::Span<uint8_t>(buffer)); + + EXPECT_EQ(sent_frame.dependency, received_frame.dependency); + EXPECT_EQ(sent_frame.frame_id, received_frame.frame_id); + EXPECT_EQ(sent_frame.referenced_frame_id, + received_frame.referenced_frame_id); + EXPECT_EQ(sent_frame.rtp_timestamp, received_frame.rtp_timestamp); + EXPECT_EQ(sent_frame.reference_time + kOneWayNetworkDelay + + SimulatedFrame::GetExpectedPlayoutDelay(sent_frame.frame_id - + FrameId::first()), + received_frame.reference_time); + EXPECT_EQ(sent_frame.new_playout_delay, received_frame.new_playout_delay); + EXPECT_EQ(sent_frame.data, received_frame.data); + } + + // Consume zero or more frames from the Receiver, verifying that they are the + // same as the SimulatedFrame that was sent. + void ConsumeAndVerifyFrames(int first, + int last, + Clock::time_point start_time) { + for (int i = first; i <= last; ++i) { + ConsumeAndVerifyFrame(SimulatedFrame(start_time, i)); + } + } + + private: + FakeClock clock_; + FakeTaskRunner task_runner_; + testing::NiceMock<MockEnvironment> env_; + ReceiverPacketRouter packet_router_; + Receiver receiver_; + testing::NiceMock<MockSender> sender_; + testing::NiceMock<MockConsumer> consumer_; +}; + +// Tests that the Receiver processes RTCP packets correctly and sends RTCP +// reports at regular intervals. +TEST_F(ReceiverTest, ReceivesAndSendsRtcpPackets) { + // Sender-side expectations, after the Receiver has processed the first Sender + // Report. + Clock::time_point receiver_reference_time{}; + EXPECT_CALL(*sender(), OnReceiverReferenceTimeAdvanced(_)) + .WillOnce(SaveArg<0>(&receiver_reference_time)); + RtcpReportBlock receiver_report; + EXPECT_CALL(*sender(), OnReceiverReport(_)) + .WillOnce(SaveArg<0>(&receiver_report)); + EXPECT_CALL(*sender(), + OnReceiverCheckpoint(FrameId::first() - 1, kTargetPlayoutDelay)) + .Times(1); + + // Have the MockSender send a Sender Report with lip-sync timing information. + const Clock::time_point sender_reference_time = FakeClock::now(); + const RtpTimeTicks sender_rtp_timestamp = + RtpTimeTicks::FromTimeSinceOrigin(seconds(1), kRtpTimebase); + const StatusReportId sender_report_id = + sender()->SendSenderReport(sender_reference_time, sender_rtp_timestamp); + AdvanceClockAndRunTasks(kOneWayNetworkDelay); + + // Expect the MockSender got back a Receiver Report that includes its SSRC and + // the last Sender Report ID. + testing::Mock::VerifyAndClearExpectations(sender()); + EXPECT_EQ(kSenderSsrc, receiver_report.ssrc); + EXPECT_EQ(sender_report_id, receiver_report.last_status_report_id); + + // Confirm the clock offset math: Since the Receiver and MockSender share the + // same underlying FakeClock, the Receiver should be 10ms ahead of the Sender, + // which reflects the simulated one-way network packet travel time (of the + // Sender Report). + // + // Note: The offset can be affected by the lossy conversion when going to and + // from the wire-format NtpTimestamps. See the unit tests in + // ntp_time_unittest.cc for further discussion. + constexpr auto kAllowedNtpRoundingError = microseconds(2); + EXPECT_NEAR(duration_cast<microseconds>(kOneWayNetworkDelay).count(), + duration_cast<microseconds>(receiver_reference_time - + sender_reference_time) + .count(), + kAllowedNtpRoundingError.count()); + + // Without the Sender doing anything, the Receiver should continue providing + // RTCP reports at regular intervals. Simulate three intervals of time, + // verifying that the Receiver did send reports. + Clock::time_point last_receiver_reference_time = receiver_reference_time; + for (int i = 0; i < 3; ++i) { + receiver_reference_time = Clock::time_point(); + EXPECT_CALL(*sender(), OnReceiverReferenceTimeAdvanced(_)) + .WillRepeatedly(SaveArg<0>(&receiver_reference_time)); + AdvanceClockAndRunTasks(kRtcpReportInterval); + testing::Mock::VerifyAndClearExpectations(sender()); + EXPECT_LT(last_receiver_reference_time, receiver_reference_time); + last_receiver_reference_time = receiver_reference_time; + } +} + +// Tests that the Receiver processes RTP packets, which might arrive in-order or +// out of order, but such that each frame is completely received in-order. Also, +// confirms that target playout delay changes are processed/applied correctly. +TEST_F(ReceiverTest, ReceivesFramesInOrder) { + // Send the initial Sender Report with lip-sync timing information to + // "unblock" the Receiver. + const Clock::time_point start_time = FakeClock::now(); + sender()->SendSenderReport(start_time, SimulatedFrame::GetRtpStartTime()); + AdvanceClockAndRunTasks(kOneWayNetworkDelay); + + EXPECT_CALL(*consumer(), OnFramesReady(Gt(0))).Times(10); + for (int i = 0; i <= 9; ++i) { + EXPECT_CALL(*sender(), OnReceiverCheckpoint( + FrameId::first() + i, + SimulatedFrame::GetExpectedPlayoutDelay(i))) + .Times(1); + EXPECT_CALL(*sender(), OnReceiverIsMissingPackets(_)).Times(0); + + sender()->SetFrameBeingSent(SimulatedFrame(start_time, i)); + // Send the frame's packets in-order half the time, out-of-order the other + // half. + const int permutation = (i % 2) ? i : 0; + sender()->SendRtpPackets(sender()->GetAllPacketIds(permutation)); + + // The Receiver should immediately ACK once it has received all the RTP + // packets to complete the frame. + RunTasksUntilIdle(); + testing::Mock::VerifyAndClearExpectations(sender()); + } + + // When the Receiver has all of the frames and they are complete, it should + // send out a low-frequency periodic RTCP "ping." Verify that there is one and + // only one "ping" sent when the clock moves forward by one default report + // interval during a period of inactivity. + EXPECT_CALL(*sender(), OnReceiverCheckpoint(FrameId::first() + 9, + kTargetPlayoutDelayChange)) + .Times(1); + AdvanceClockAndRunTasks(kRtcpReportInterval); + testing::Mock::VerifyAndClearExpectations(sender()); + + ConsumeAndVerifyFrames(0, 9, start_time); + EXPECT_EQ(Receiver::kNoFramesReady, receiver()->AdvanceToNextFrame()); +} + +// Tests that the Receiver processes RTP packets, can receive frames out of +// order, and issues the appropriate ACK/NACK feedback to the Sender as it +// realizes what it has and what it's missing. +TEST_F(ReceiverTest, ReceivesFramesOutOfOrder) { + // Send the initial Sender Report with lip-sync timing information to + // "unblock" the Receiver. + const Clock::time_point start_time = FakeClock::now(); + sender()->SendSenderReport(start_time, SimulatedFrame::GetRtpStartTime()); + AdvanceClockAndRunTasks(kOneWayNetworkDelay); + + constexpr static int kOutOfOrderFrames[] = {3, 4, 2, 0, 1}; + for (int i : kOutOfOrderFrames) { + // Expectations are different as each frame is sent and received. + switch (i) { + case 3: { + // Note that frame 4 will not yet be known to the Receiver, and so it + // should not be mentioned in any of the feedback for this case. + EXPECT_CALL(*sender(), OnReceiverCheckpoint(FrameId::first() - 1, + kTargetPlayoutDelay)) + .Times(AtLeast(1)); + EXPECT_CALL( + *sender(), + OnReceiverHasFrames(std::vector<FrameId>({FrameId::first() + 3}))) + .Times(AtLeast(1)); + EXPECT_CALL(*sender(), + OnReceiverIsMissingPackets(std::vector<PacketNack>({ + PacketNack{FrameId::first(), kAllPacketsLost}, + PacketNack{FrameId::first() + 1, kAllPacketsLost}, + PacketNack{FrameId::first() + 2, kAllPacketsLost}, + }))) + .Times(AtLeast(1)); + EXPECT_CALL(*consumer(), OnFramesReady(_)).Times(0); + break; + } + + case 4: { + EXPECT_CALL(*sender(), OnReceiverCheckpoint(FrameId::first() - 1, + kTargetPlayoutDelay)) + .Times(AtLeast(1)); + EXPECT_CALL(*sender(), + OnReceiverHasFrames(std::vector<FrameId>( + {FrameId::first() + 3, FrameId::first() + 4}))) + .Times(AtLeast(1)); + EXPECT_CALL(*sender(), + OnReceiverIsMissingPackets(std::vector<PacketNack>({ + PacketNack{FrameId::first(), kAllPacketsLost}, + PacketNack{FrameId::first() + 1, kAllPacketsLost}, + PacketNack{FrameId::first() + 2, kAllPacketsLost}, + }))) + .Times(AtLeast(1)); + EXPECT_CALL(*consumer(), OnFramesReady(_)).Times(0); + break; + } + + case 2: { + EXPECT_CALL(*sender(), OnReceiverCheckpoint(FrameId::first() - 1, + kTargetPlayoutDelay)) + .Times(AtLeast(1)); + EXPECT_CALL(*sender(), OnReceiverHasFrames(std::vector<FrameId>( + {FrameId::first() + 2, FrameId::first() + 3, + FrameId::first() + 4}))) + .Times(AtLeast(1)); + EXPECT_CALL(*sender(), + OnReceiverIsMissingPackets(std::vector<PacketNack>({ + PacketNack{FrameId::first(), kAllPacketsLost}, + PacketNack{FrameId::first() + 1, kAllPacketsLost}, + }))) + .Times(AtLeast(1)); + EXPECT_CALL(*consumer(), OnFramesReady(_)).Times(0); + break; + } + + case 0: { + EXPECT_CALL(*sender(), + OnReceiverCheckpoint(FrameId::first(), kTargetPlayoutDelay)) + .Times(AtLeast(1)); + EXPECT_CALL(*sender(), OnReceiverHasFrames(std::vector<FrameId>( + {FrameId::first() + 2, FrameId::first() + 3, + FrameId::first() + 4}))) + .Times(AtLeast(1)); + EXPECT_CALL(*sender(), + OnReceiverIsMissingPackets(std::vector<PacketNack>( + {PacketNack{FrameId::first() + 1, kAllPacketsLost}}))) + .Times(AtLeast(1)); + EXPECT_CALL(*consumer(), OnFramesReady(Gt(0))).Times(1); + break; + } + + case 1: { + EXPECT_CALL(*sender(), OnReceiverCheckpoint(FrameId::first() + 4, + kTargetPlayoutDelay)) + .Times(AtLeast(1)); + EXPECT_CALL(*sender(), OnReceiverHasFrames(_)).Times(0); + EXPECT_CALL(*sender(), OnReceiverIsMissingPackets(_)).Times(0); + EXPECT_CALL(*consumer(), OnFramesReady(Gt(0))).Times(1); + break; + } + + default: + OSP_NOTREACHED(); + } + + sender()->SetFrameBeingSent(SimulatedFrame(start_time, i)); + sender()->SendRtpPackets(sender()->GetAllPacketIds(i)); + + // While there are known incomplete frames, the Receiver should send RTCP + // packets more frequently than the default "ping" interval. Thus, advancing + // the clock by this much should result in several feedback reports + // transmitted to the Sender. + AdvanceClockAndRunTasks(kRtcpReportInterval); + + testing::Mock::VerifyAndClearExpectations(sender()); + testing::Mock::VerifyAndClearExpectations(consumer()); + } + + ConsumeAndVerifyFrames(0, 4, start_time); + EXPECT_EQ(Receiver::kNoFramesReady, receiver()->AdvanceToNextFrame()); +} + +// Tests that the Receiver will respond to a key frame request from its client +// by sending a Picture Loss Indicator (PLI) to the Sender, and then will +// automatically stop sending the PLI once a key frame has been received. +TEST_F(ReceiverTest, RequestsKeyFrameToRectifyPictureLoss) { + // Send the initial Sender Report with lip-sync timing information to + // "unblock" the Receiver. + const Clock::time_point start_time = FakeClock::now(); + sender()->SendSenderReport(start_time, SimulatedFrame::GetRtpStartTime()); + AdvanceClockAndRunTasks(kOneWayNetworkDelay); + + // Send and Receive three frames in-order, normally. + for (int i = 0; i <= 2; ++i) { + EXPECT_CALL(*consumer(), OnFramesReady(Gt(0))).Times(1); + EXPECT_CALL(*sender(), + OnReceiverCheckpoint(FrameId::first() + i, kTargetPlayoutDelay)) + .Times(1); + sender()->SetFrameBeingSent(SimulatedFrame(start_time, i)); + sender()->SendRtpPackets(sender()->GetAllPacketIds(0)); + RunTasksUntilIdle(); + testing::Mock::VerifyAndClearExpectations(sender()); + testing::Mock::VerifyAndClearExpectations(consumer()); + } + ConsumeAndVerifyFrames(0, 2, start_time); + + // Simulate the Consumer requesting a key frame after picture loss (e.g., a + // decoder failure). Ensure the Sender is immediately notified. + EXPECT_CALL(*sender(), OnReceiverIndicatesPictureLoss()).Times(1); + receiver()->RequestKeyFrame(); + RunTasksUntilIdle(); + testing::Mock::VerifyAndClearExpectations(sender()); + + // The Sender sends another frame that is not a key frame and, upon receipt, + // the Receiver should repeat its "cry" for a key frame. + EXPECT_CALL(*consumer(), OnFramesReady(Gt(0))).Times(1); + EXPECT_CALL(*sender(), + OnReceiverCheckpoint(FrameId::first() + 3, kTargetPlayoutDelay)) + .Times(1); + EXPECT_CALL(*sender(), OnReceiverIndicatesPictureLoss()).Times(1); + sender()->SetFrameBeingSent(SimulatedFrame(start_time, 3)); + sender()->SendRtpPackets(sender()->GetAllPacketIds(0)); + RunTasksUntilIdle(); + testing::Mock::VerifyAndClearExpectations(sender()); + testing::Mock::VerifyAndClearExpectations(consumer()); + ConsumeAndVerifyFrames(3, 3, start_time); + + // Finally, the Sender responds to the PLI condition by sending a key frame. + // Confirm the Receiver has stopped indicating picture loss after having + // received the key frame. + EXPECT_CALL(*consumer(), OnFramesReady(Gt(0))).Times(1); + EXPECT_CALL(*sender(), + OnReceiverCheckpoint(FrameId::first() + 4, kTargetPlayoutDelay)) + .Times(1); + EXPECT_CALL(*sender(), OnReceiverIndicatesPictureLoss()).Times(0); + SimulatedFrame key_frame(start_time, 4); + key_frame.dependency = EncodedFrame::KEY_FRAME; + key_frame.referenced_frame_id = key_frame.frame_id; + sender()->SetFrameBeingSent(key_frame); + sender()->SendRtpPackets(sender()->GetAllPacketIds(0)); + RunTasksUntilIdle(); + testing::Mock::VerifyAndClearExpectations(sender()); + testing::Mock::VerifyAndClearExpectations(consumer()); + + // The client has not yet consumed the key frame, so any calls to + // RequestKeyFrame() should not set the PLI condition again. + EXPECT_CALL(*sender(), OnReceiverIndicatesPictureLoss()).Times(0); + receiver()->RequestKeyFrame(); + RunTasksUntilIdle(); + testing::Mock::VerifyAndClearExpectations(sender()); + + // After consuming the requested key frame, the client should be able to set + // the PLI condition again with another RequestKeyFrame() call. + ConsumeAndVerifyFrame(key_frame); + EXPECT_CALL(*sender(), OnReceiverIndicatesPictureLoss()).Times(1); + receiver()->RequestKeyFrame(); + RunTasksUntilIdle(); + testing::Mock::VerifyAndClearExpectations(sender()); +} + +// Tests that the Receiver will start dropping packets once its frame queue is +// full (i.e., when the consumer is not pulling them out of the queue). Since +// the Receiver will stop ACK'ing frames, the Sender will become stalled. +TEST_F(ReceiverTest, EatsItsFill) { + // Send the initial Sender Report with lip-sync timing information to + // "unblock" the Receiver. + const Clock::time_point start_time = FakeClock::now(); + sender()->SendSenderReport(start_time, SimulatedFrame::GetRtpStartTime()); + AdvanceClockAndRunTasks(kOneWayNetworkDelay); + + // Send and Receive the maximum possible number of frames in-order, normally. + for (int i = 0; i < kMaxUnackedFrames; ++i) { + EXPECT_CALL(*consumer(), OnFramesReady(Gt(0))).Times(1); + EXPECT_CALL(*sender(), OnReceiverCheckpoint( + FrameId::first() + i, + SimulatedFrame::GetExpectedPlayoutDelay(i))) + .Times(1); + sender()->SetFrameBeingSent(SimulatedFrame(start_time, i)); + sender()->SendRtpPackets(sender()->GetAllPacketIds(0)); + RunTasksUntilIdle(); + testing::Mock::VerifyAndClearExpectations(sender()); + testing::Mock::VerifyAndClearExpectations(consumer()); + } + + // Sending one more frame should be ignored. Over and over. None of the + // feedback reports from the Receiver should indicate it is collecting packets + // for future frames. + int ignored_frame = kMaxUnackedFrames; + for (int i = 0; i < 5; ++i) { + EXPECT_CALL(*consumer(), OnFramesReady(_)).Times(0); + EXPECT_CALL(*sender(), + OnReceiverCheckpoint(FrameId::first() + (ignored_frame - 1), + kTargetPlayoutDelayChange)) + .Times(AtLeast(1)); + EXPECT_CALL(*sender(), OnReceiverIsMissingPackets(_)).Times(0); + sender()->SetFrameBeingSent(SimulatedFrame(start_time, ignored_frame)); + sender()->SendRtpPackets(sender()->GetAllPacketIds(0)); + AdvanceClockAndRunTasks(kRtcpReportInterval); + testing::Mock::VerifyAndClearExpectations(sender()); + testing::Mock::VerifyAndClearExpectations(consumer()); + } + + // Consume only one frame, and confirm the Receiver allows only one frame more + // to be received. + ConsumeAndVerifyFrames(0, 0, start_time); + int no_longer_ignored_frame = ignored_frame; + ++ignored_frame; + EXPECT_CALL(*consumer(), OnFramesReady(Gt(0))).Times(1); + EXPECT_CALL(*sender(), + OnReceiverCheckpoint(FrameId::first() + no_longer_ignored_frame, + kTargetPlayoutDelayChange)) + .Times(AtLeast(1)); + EXPECT_CALL(*sender(), OnReceiverIsMissingPackets(_)).Times(0); + // This frame should be received successfully. + sender()->SetFrameBeingSent( + SimulatedFrame(start_time, no_longer_ignored_frame)); + sender()->SendRtpPackets(sender()->GetAllPacketIds(0)); + // This second frame should be ignored, however. + sender()->SetFrameBeingSent(SimulatedFrame(start_time, ignored_frame)); + sender()->SendRtpPackets(sender()->GetAllPacketIds(0)); + AdvanceClockAndRunTasks(kRtcpReportInterval); + testing::Mock::VerifyAndClearExpectations(sender()); + testing::Mock::VerifyAndClearExpectations(consumer()); +} + +// Tests that incomplete frames that would be played-out too late are dropped, +// but only as inter-frame data dependency requirements permit, and only if no +// target playout delay change information would have been missed. +TEST_F(ReceiverTest, DropsLateFrames) { + // Send the initial Sender Report with lip-sync timing information to + // "unblock" the Receiver. + const Clock::time_point start_time = FakeClock::now(); + sender()->SendSenderReport(start_time, SimulatedFrame::GetRtpStartTime()); + AdvanceClockAndRunTasks(kOneWayNetworkDelay); + + // Before any packets have been sent/received, the Receiver should indicate no + // frames are ready. + EXPECT_EQ(Receiver::kNoFramesReady, receiver()->AdvanceToNextFrame()); + + // Set a ridiculously-large estimated player processing time so that the logic + // thinks every frame going to play out too late. + receiver()->SetPlayerProcessingTime(seconds(3)); + + // In this test there are eight frames total: + // - Frame 0: Key frame. + // - Frames 1-4: Non-key frames. + // - Frame 5: Non-key frame that contains a target playout delay change. + // - Frame 6: Key frame. + // - Frame 7: Non-key frame. + ASSERT_EQ(SimulatedFrame::kPlayoutChangeAtFrame, 5); + SimulatedFrame frames[8] = {{start_time, 0}, {start_time, 1}, {start_time, 2}, + {start_time, 3}, {start_time, 4}, {start_time, 5}, + {start_time, 6}, {start_time, 7}}; + frames[6].dependency = EncodedFrame::KEY_FRAME; + frames[6].referenced_frame_id = frames[6].frame_id; + + // Send just packet 1 (NOT packet 0) of all the frames. The Receiver should + // never notify the consumer via the callback, nor report that any frames are + // ready, because none of the frames have been completely received. + EXPECT_CALL(*consumer(), OnFramesReady(_)).Times(0); + EXPECT_CALL(*sender(), OnReceiverCheckpoint(_, _)).Times(0); + for (int i = 0; i <= 7; ++i) { + sender()->SetFrameBeingSent(frames[i]); + // Assumption: There are at least three packets in each frame, else the test + // is not exercising the logic meaningfully. + ASSERT_LE(size_t{3}, sender()->GetAllPacketIds(0).size()); + sender()->SendRtpPackets({FramePacketId{1}}); + } + RunTasksUntilIdle(); + testing::Mock::VerifyAndClearExpectations(consumer()); + testing::Mock::VerifyAndClearExpectations(sender()); + EXPECT_EQ(Receiver::kNoFramesReady, receiver()->AdvanceToNextFrame()); + + // Send all the packets of Frame 6 (the second key frame) and Frame 7. The + // Receiver still cannot drop any frames because it has not seen packet 0 of + // every prior frame. In other words, it cannot ignore any possibility of a + // target playout delay change from the Sender. + EXPECT_CALL(*consumer(), OnFramesReady(_)).Times(0); + EXPECT_CALL(*sender(), OnReceiverCheckpoint(_, _)).Times(0); + for (int i = 6; i <= 7; ++i) { + sender()->SetFrameBeingSent(frames[i]); + sender()->SendRtpPackets(sender()->GetAllPacketIds(0)); + } + RunTasksUntilIdle(); + testing::Mock::VerifyAndClearExpectations(consumer()); + testing::Mock::VerifyAndClearExpectations(sender()); + EXPECT_EQ(Receiver::kNoFramesReady, receiver()->AdvanceToNextFrame()); + + // Send packet 0 for all but Frame 5, which contains a target playout delay + // change. All but the last two frames will still be incomplete. The Receiver + // still cannot drop any frames because it doesn't know whether Frame 5 had a + // target playout delay change. + EXPECT_CALL(*consumer(), OnFramesReady(_)).Times(0); + EXPECT_CALL(*sender(), OnReceiverCheckpoint(_, _)).Times(0); + for (int i = 0; i <= 7; ++i) { + if (i == 5) { + continue; + } + sender()->SetFrameBeingSent(frames[i]); + sender()->SendRtpPackets({FramePacketId{0}}); + } + RunTasksUntilIdle(); + testing::Mock::VerifyAndClearExpectations(consumer()); + testing::Mock::VerifyAndClearExpectations(sender()); + EXPECT_EQ(Receiver::kNoFramesReady, receiver()->AdvanceToNextFrame()); + + // Finally, send packet 0 for Frame 5. Now, the Receiver will drop every frame + // before the completely-received second key frame, as they are all still + // incomplete and will play-out too late. When it drops the frames, it will + // notify the sender of the new checkpoint so that it stops trying to + // re-transmit the dropped frames. + EXPECT_CALL(*consumer(), OnFramesReady(Gt(0))).Times(1); + EXPECT_CALL(*sender(), OnReceiverCheckpoint(FrameId::first() + 7, + kTargetPlayoutDelayChange)) + .Times(1); + sender()->SetFrameBeingSent(frames[5]); + sender()->SendRtpPackets({FramePacketId{0}}); + RunTasksUntilIdle(); + // Note: Consuming Frame 6 will trigger the checkpoint advancement, since the + // call to AdvanceToNextFrame() contains the frame skipping/dropping logic. + ConsumeAndVerifyFrame(frames[6]); + testing::Mock::VerifyAndClearExpectations(consumer()); + testing::Mock::VerifyAndClearExpectations(sender()); + + // After consuming Frame 6, the Receiver knows Frame 7 is also available and + // should have scheduled an immediate task to notify the Consumer of this. + EXPECT_CALL(*consumer(), OnFramesReady(Gt(0))).Times(1); + RunTasksUntilIdle(); + testing::Mock::VerifyAndClearExpectations(consumer()); + + // Now consume Frame 7. This shouldn't trigger any further checkpoint + // advancement. + EXPECT_CALL(*consumer(), OnFramesReady(_)).Times(0); + EXPECT_CALL(*sender(), OnReceiverCheckpoint(_, _)).Times(0); + ConsumeAndVerifyFrame(frames[7]); + testing::Mock::VerifyAndClearExpectations(consumer()); + testing::Mock::VerifyAndClearExpectations(sender()); +} + +} // namespace +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/rtcp_common.cc b/cast/streaming/rtcp_common.cc new file mode 100644 index 00000000..30d587a2 --- /dev/null +++ b/cast/streaming/rtcp_common.cc @@ -0,0 +1,246 @@ +// 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/rtcp_common.h" + +#include <limits> + +#include "cast/streaming/packet_util.h" +#include "util/saturate_cast.h" + +using openscreen::saturate_cast; +using openscreen::platform::Clock; + +namespace cast { +namespace streaming { + +RtcpCommonHeader::RtcpCommonHeader() = default; +RtcpCommonHeader::~RtcpCommonHeader() = default; + +void RtcpCommonHeader::AppendFields(absl::Span<uint8_t>* buffer) const { + OSP_CHECK_GE(buffer->size(), kRtcpCommonHeaderSize); + + uint8_t byte0 = kRtcpRequiredVersionAndPaddingBits + << kRtcpReportCountFieldNumBits; + switch (packet_type) { + case RtcpPacketType::kSenderReport: + case RtcpPacketType::kReceiverReport: + OSP_DCHECK_LE(with.report_count, + FieldBitmask<int>(kRtcpReportCountFieldNumBits)); + byte0 |= with.report_count; + break; + case RtcpPacketType::kApplicationDefined: + case RtcpPacketType::kPayloadSpecific: + switch (with.subtype) { + case RtcpSubtype::kPictureLossIndicator: + case RtcpSubtype::kFeedback: + byte0 |= static_cast<uint8_t>(with.subtype); + break; + case RtcpSubtype::kReceiverLog: + OSP_UNIMPLEMENTED(); + break; + default: + OSP_NOTREACHED(); + break; + } + break; + case RtcpPacketType::kExtendedReports: + break; + case RtcpPacketType::kNull: + OSP_NOTREACHED(); + break; + } + AppendField<uint8_t>(byte0, buffer); + + AppendField<uint8_t>(static_cast<uint8_t>(packet_type), buffer); + + // The size of the packet must be evenly divisible by the 32-bit word size. + OSP_DCHECK_EQ(0, payload_size % sizeof(uint32_t)); + AppendField<uint16_t>(payload_size / sizeof(uint32_t), buffer); +} + +// static +absl::optional<RtcpCommonHeader> RtcpCommonHeader::Parse( + absl::Span<const uint8_t> buffer) { + if (buffer.size() < kRtcpCommonHeaderSize) { + return absl::nullopt; + } + + const uint8_t byte0 = ConsumeField<uint8_t>(&buffer); + if ((byte0 >> kRtcpReportCountFieldNumBits) != + kRtcpRequiredVersionAndPaddingBits) { + return absl::nullopt; + } + const uint8_t report_count_or_subtype = + byte0 & FieldBitmask<uint8_t>(kRtcpReportCountFieldNumBits); + + const uint8_t byte1 = ConsumeField<uint8_t>(&buffer); + if (!IsRtcpPacketType(byte1)) { + return absl::nullopt; + } + + // Optionally set |header.with.report_count| or |header.with.subtype|, + // depending on the packet type. + RtcpCommonHeader header; + header.packet_type = static_cast<RtcpPacketType>(byte1); + switch (header.packet_type) { + case RtcpPacketType::kSenderReport: + case RtcpPacketType::kReceiverReport: + header.with.report_count = report_count_or_subtype; + break; + case RtcpPacketType::kApplicationDefined: + case RtcpPacketType::kPayloadSpecific: + switch (static_cast<RtcpSubtype>(report_count_or_subtype)) { + case RtcpSubtype::kPictureLossIndicator: + case RtcpSubtype::kReceiverLog: + case RtcpSubtype::kFeedback: + header.with.subtype = + static_cast<RtcpSubtype>(report_count_or_subtype); + break; + default: // Unknown subtype. + header.with.subtype = RtcpSubtype::kNull; + break; + } + break; + default: + // Neither |header.with.report_count| nor |header.with.subtype| are used. + break; + } + + header.payload_size = + static_cast<int>(ConsumeField<uint16_t>(&buffer)) * sizeof(uint32_t); + + return header; +} + +RtcpReportBlock::RtcpReportBlock() = default; +RtcpReportBlock::~RtcpReportBlock() = default; + +void RtcpReportBlock::AppendFields(absl::Span<uint8_t>* buffer) const { + OSP_CHECK_GE(buffer->size(), kRtcpReportBlockSize); + + AppendField<uint32_t>(ssrc, buffer); + OSP_DCHECK_GE(packet_fraction_lost_numerator, + std::numeric_limits<uint8_t>::min()); + OSP_DCHECK_LE(packet_fraction_lost_numerator, + std::numeric_limits<uint8_t>::max()); + OSP_DCHECK_GE(cumulative_packets_lost, 0); + OSP_DCHECK_LE(cumulative_packets_lost, + FieldBitmask<int>(kRtcpCumulativePacketsFieldNumBits)); + AppendField<uint32_t>( + (static_cast<int>(packet_fraction_lost_numerator) + << kRtcpCumulativePacketsFieldNumBits) | + (static_cast<int>(cumulative_packets_lost) & + FieldBitmask<uint32_t>(kRtcpCumulativePacketsFieldNumBits)), + buffer); + AppendField<uint32_t>(extended_high_sequence_number, buffer); + const int64_t jitter_ticks = jitter / RtpTimeDelta::FromTicks(1); + OSP_DCHECK_GE(jitter_ticks, 0); + OSP_DCHECK_LE(jitter_ticks, int64_t{std::numeric_limits<uint32_t>::max()}); + AppendField<uint32_t>(jitter_ticks, buffer); + AppendField<uint32_t>(last_status_report_id, buffer); + const int64_t delay_ticks = delay_since_last_report.count(); + OSP_DCHECK_GE(delay_ticks, 0); + OSP_DCHECK_LE(delay_ticks, int64_t{std::numeric_limits<uint32_t>::max()}); + AppendField<uint32_t>(delay_ticks, buffer); +} + +void RtcpReportBlock::SetPacketFractionLostNumerator( + int64_t num_apparently_sent, + int64_t num_received) { + if (num_apparently_sent <= 0) { + packet_fraction_lost_numerator = 0; + return; + } + // The following computes the fraction of packets lost as "one minus + // |num_received| divided by |num_apparently_sent|" and scales by 256 (the + // kPacketFractionLostDenominator). It's valid for |num_received| to be + // greater than |num_apparently_sent| in some cases (e.g., if duplicate + // packets were received from the network). + const int64_t numerator = + ((num_apparently_sent - num_received) * kPacketFractionLostDenominator) / + num_apparently_sent; + // Since the value must be in the range [0,255], just do a saturate_cast + // to the uint8_t type to clamp. + packet_fraction_lost_numerator = saturate_cast<uint8_t>(numerator); +} + +void RtcpReportBlock::SetCumulativePacketsLost(int64_t num_apparently_sent, + int64_t num_received) { + const int64_t num_lost = num_apparently_sent - num_received; + // Clamp to valid range supported by the wire format (and RTP spec). + // + // Note that |num_lost| can be negative if duplicate packets were received. + // The RFC spec (https://tools.ietf.org/html/rfc3550#section-6.4.1) states + // this should result in a clamped, "zero loss" value. + cumulative_packets_lost = static_cast<int>( + std::min(std::max<int64_t>(num_lost, 0), + FieldBitmask<int64_t>(kRtcpCumulativePacketsFieldNumBits))); +} + +void RtcpReportBlock::SetDelaySinceLastReport( + Clock::duration local_clock_delay) { + // Clamp to valid range supported by the wire format (and RTP spec). The + // bounds checking is done in terms of Clock::duration, since doing the checks + // after the duration_cast may allow overflow to occur in the duration_cast + // math (well, only for unusually large inputs). + constexpr Delay kMaxValidReportedDelay{std::numeric_limits<uint32_t>::max()}; + constexpr auto kMaxValidLocalClockDelay = + std::chrono::duration_cast<Clock::duration>(kMaxValidReportedDelay); + if (local_clock_delay > kMaxValidLocalClockDelay) { + delay_since_last_report = kMaxValidReportedDelay; + return; + } + if (local_clock_delay <= Clock::duration::zero()) { + delay_since_last_report = Delay::zero(); + return; + } + + // If this point is reached, then the |local_clock_delay| is representable as + // a Delay within the valid range. + delay_since_last_report = + std::chrono::duration_cast<Delay>(local_clock_delay); +} + +// static +absl::optional<RtcpReportBlock> RtcpReportBlock::ParseOne( + absl::Span<const uint8_t> buffer, + int report_count, + Ssrc ssrc) { + if (static_cast<int>(buffer.size()) < (kRtcpReportBlockSize * report_count)) { + return absl::nullopt; + } + + absl::optional<RtcpReportBlock> result; + for (int block = 0; block < report_count; ++block) { + if (ConsumeField<uint32_t>(&buffer) != ssrc) { + // Skip-over report block meant for some other recipient. + buffer.remove_prefix(kRtcpReportBlockSize - sizeof(uint32_t)); + continue; + } + + RtcpReportBlock& report_block = result.emplace(); + report_block.ssrc = ssrc; + const auto second_word = ConsumeField<uint32_t>(&buffer); + report_block.packet_fraction_lost_numerator = + second_word >> kRtcpCumulativePacketsFieldNumBits; + report_block.cumulative_packets_lost = + second_word & + FieldBitmask<uint32_t>(kRtcpCumulativePacketsFieldNumBits); + report_block.extended_high_sequence_number = + ConsumeField<uint32_t>(&buffer); + report_block.jitter = + RtpTimeDelta::FromTicks(ConsumeField<uint32_t>(&buffer)); + report_block.last_status_report_id = ConsumeField<uint32_t>(&buffer); + report_block.delay_since_last_report = + RtcpReportBlock::Delay(ConsumeField<uint32_t>(&buffer)); + } + return result; +} + +RtcpSenderReport::RtcpSenderReport() = default; +RtcpSenderReport::~RtcpSenderReport() = default; + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/rtcp_common.h b/cast/streaming/rtcp_common.h new file mode 100644 index 00000000..3b6959c0 --- /dev/null +++ b/cast/streaming/rtcp_common.h @@ -0,0 +1,184 @@ +// 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. + +#ifndef CAST_STREAMING_RTCP_COMMON_H_ +#define CAST_STREAMING_RTCP_COMMON_H_ + +#include <stdint.h> + +#include <tuple> +#include <vector> + +#include "absl/types/optional.h" +#include "absl/types/span.h" +#include "cast/streaming/frame_id.h" +#include "cast/streaming/ntp_time.h" +#include "cast/streaming/rtp_defines.h" +#include "cast/streaming/rtp_time.h" +#include "cast/streaming/ssrc.h" + +namespace cast { +namespace streaming { + +struct RtcpCommonHeader { + RtcpCommonHeader(); + ~RtcpCommonHeader(); + + RtcpPacketType packet_type = RtcpPacketType::kNull; + + union { + // The number of report blocks if |packet_type| is kSenderReport or + // kReceiverReport. + int report_count; + + // Indicates the type of an application-defined message if |packet_type| is + // kApplicationDefined or kPayloadSpecific. + RtcpSubtype subtype; + + // Otherwise, not used. + } with{0}; + + // The size (in bytes) of the RTCP packet, not including the header. + int payload_size = 0; + + // Serializes this header into the first |kRtcpCommonHeaderSize| bytes of the + // given |buffer| and adjusts |buffer| to point to the first byte after it. + void AppendFields(absl::Span<uint8_t>* buffer) const; + + // Parse from the 4-byte wire format in |buffer|. Returns nullopt if the data + // is corrupt. + static absl::optional<RtcpCommonHeader> Parse( + absl::Span<const uint8_t> buffer); +}; + +// The middle 32-bits of the 64-bit NtpTimestamp field from the Sender Reports. +// This is used as an opaque identifier that the Receiver will use in its +// reports to refer to specific previous Sender Reports. +using StatusReportId = uint32_t; +constexpr StatusReportId ToStatusReportId(NtpTimestamp ntp_timestamp) { + return static_cast<uint32_t>(ntp_timestamp >> 16); +} + +// One of these is optionally included with a Sender Report or a Receiver +// Report. See: https://tools.ietf.org/html/rfc3550#section-6.4.1 +struct RtcpReportBlock { + RtcpReportBlock(); + ~RtcpReportBlock(); + + // The intended recipient of this report block. + Ssrc ssrc = 0; + + // The fraction of RTP packets lost since the last report, specified as a + // variable numerator and fixed denominator. The numerator will always be in + // the range [0,255] since, semantically: + // + // a. Negative values are impossible. + // b. Values greater than 255 would indicate 100% packet loss, and so a + // report block would not be generated in the first place. + int packet_fraction_lost_numerator = 0; + static constexpr int kPacketFractionLostDenominator = 256; + + // The total number of RTP packets lost since the start of the session. This + // value will always be in the range [0,2^24-1], as the wire format only + // provides 24 bits; so, wrap-around is possible. + int cumulative_packets_lost = 0; + + // The highest sequence number received in any RTP packet. Wrap-around is + // possible. + uint32_t extended_high_sequence_number = 0; + + // An estimate of the recent variance in RTP packet arrival times. + RtpTimeDelta jitter; + + // The last Status Report received. + StatusReportId last_status_report_id{}; + + // The delay between when the peer received the most-recent Status Report and + // when this report was sent. The timebase is 65536 ticks per second and, + // because of the wire format, this value will always be in the range + // [0,65536) seconds. + using Delay = std::chrono::duration<int64_t, std::ratio<1, 65536>>; + Delay delay_since_last_report{}; + + // Convenience helper to compute/assign the |packet_fraction_lost_numerator|, + // based on the |num_apparently_sent| and |num_received| packet counts since + // the last report was sent. + void SetPacketFractionLostNumerator(int64_t num_apparently_sent, + int64_t num_received); + + // Convenience helper to compute/assign the |cumulative_packets_lost|, based + // on the |num_apparently_sent| and |num_received| packet counts since the + // start of the entire session. + void SetCumulativePacketsLost(int64_t num_apparently_sent, + int64_t num_received); + + // Convenience helper to convert the given |local_clock_delay| to the + // RtcpReportBlock::Delay timebase, then clamp and assign it to + // |delay_since_last_report|. + void SetDelaySinceLastReport( + openscreen::platform::Clock::duration local_clock_delay); + + // Serializes this report block in the first |kRtcpReportBlockSize| bytes of + // the given |buffer| and adjusts |buffer| to point to the first byte after + // it. + void AppendFields(absl::Span<uint8_t>* buffer) const; + + // Scans the wire-format report blocks in |buffer|, searching for one with the + // matching |ssrc| and, if found, returns the parse result. Returns nullopt if + // the data is corrupt or no report block with the matching SSRC was found. + static absl::optional<RtcpReportBlock> + ParseOne(absl::Span<const uint8_t> buffer, int report_count, Ssrc ssrc); +}; + +struct RtcpSenderReport { + RtcpSenderReport(); + ~RtcpSenderReport(); + + // The point-in-time at which this report was sent, according to both: 1) the + // common reference clock shared by all RTP streams; 2) the RTP timestamp on + // the media capture/playout timeline. Together, these are used by a Receiver + // to achieve A/V synchronization across RTP streams for playout. + openscreen::platform::Clock::time_point reference_time{}; + RtpTimeTicks rtp_timestamp; + + // The total number of RTP packets transmitted since the start of the session + // (wrap-around is possible). + uint32_t send_packet_count = 0; + + // The total number of payload bytes transmitted in RTP packets since the + // start of the session (wrap-around is possible). + uint32_t send_octet_count = 0; + + // The report block, if present. While the RTCP spec allows for zero or + // multiple reports, Cast Streaming only uses zero or one. + absl::optional<RtcpReportBlock> report_block; +}; + +// A pair of IDs that refers to a specific missing packet within a frame. If +// |packet_id| is kAllPacketsLost, then it represents all the packets of a +// frame. +struct PacketNack { + FrameId frame_id; + FramePacketId packet_id; + + // Comparison operators. Define more when you need them! + // TODO(miu): In C++20, just + // replace all of this with one operator<=>() definition to get them all for + // free. + constexpr bool operator==(const PacketNack& other) const { + return frame_id == other.frame_id && packet_id == other.packet_id; + } + constexpr bool operator!=(const PacketNack& other) const { + return frame_id != other.frame_id || packet_id != other.packet_id; + } + constexpr bool operator<(const PacketNack& other) const { + return (frame_id < other.frame_id) || + (frame_id == other.frame_id && packet_id < other.packet_id); + } +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_RTCP_COMMON_H_ diff --git a/cast/streaming/rtcp_common_unittest.cc b/cast/streaming/rtcp_common_unittest.cc new file mode 100644 index 00000000..8d088c2c --- /dev/null +++ b/cast/streaming/rtcp_common_unittest.cc @@ -0,0 +1,312 @@ +// 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/rtcp_common.h" + +#include <chrono> +#include <limits> + +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "platform/api/time.h" + +using openscreen::platform::Clock; + +namespace cast { +namespace streaming { +namespace { + +template <typename T> +void SerializeAndExpectPointerAdvanced(const T& source, + int num_bytes, + uint8_t* buffer) { + absl::Span<uint8_t> buffer_span(buffer, num_bytes); + source.AppendFields(&buffer_span); + EXPECT_EQ(buffer + num_bytes, buffer_span.data()); +} + +// Tests that the RTCP Common Header for a packet type that includes an Item +// Count is successfully serialized and re-parsed. +TEST(RtcpCommonTest, SerializesAndParsesHeaderForSenderReports) { + RtcpCommonHeader original; + original.packet_type = RtcpPacketType::kSenderReport; + original.with.report_count = 31; + original.payload_size = 16; + + uint8_t buffer[kRtcpCommonHeaderSize]; + SerializeAndExpectPointerAdvanced(original, kRtcpCommonHeaderSize, buffer); + + const auto parsed = RtcpCommonHeader::Parse(buffer); + ASSERT_TRUE(parsed.has_value()); + EXPECT_EQ(original.packet_type, parsed->packet_type); + EXPECT_EQ(original.with.report_count, parsed->with.report_count); + EXPECT_EQ(original.payload_size, parsed->payload_size); +} + +// Tests that the RTCP Common Header for a packet type that includes a RTCP +// Subtype is successfully serialized and re-parsed. +TEST(RtcpCommonTest, SerializesAndParsesHeaderForCastFeedback) { + RtcpCommonHeader original; + original.packet_type = RtcpPacketType::kPayloadSpecific; + original.with.subtype = RtcpSubtype::kFeedback; + original.payload_size = 99 * sizeof(uint32_t); + + uint8_t buffer[kRtcpCommonHeaderSize]; + SerializeAndExpectPointerAdvanced(original, kRtcpCommonHeaderSize, buffer); + + const auto parsed = RtcpCommonHeader::Parse(buffer); + ASSERT_TRUE(parsed.has_value()); + EXPECT_EQ(original.packet_type, parsed->packet_type); + EXPECT_EQ(original.with.subtype, parsed->with.subtype); + EXPECT_EQ(original.payload_size, parsed->payload_size); +} + +// Tests that a RTCP Common Header will not be parsed from an empty buffer. +TEST(RtcpCommonTest, WillNotParseHeaderFromEmptyBuffer) { + const uint8_t kEmptyPacket[] = {}; + EXPECT_FALSE( + RtcpCommonHeader::Parse(absl::Span<const uint8_t>(kEmptyPacket, 0)) + .has_value()); +} + +// Tests that a RTCP Common Header will not be parsed from a buffer containing +// garbage data. +TEST(RtcpCommonTest, WillNotParseHeaderFromGarbage) { + // clang-format off + const uint8_t kGarbage[] = { + 0x4f, 0x27, 0xeb, 0x22, 0x27, 0xeb, 0x22, 0x4f, + 0xeb, 0x22, 0x4f, 0x27, 0x22, 0x4f, 0x27, 0xeb, + }; + // clang-format on + EXPECT_FALSE(RtcpCommonHeader::Parse(kGarbage).has_value()); +} + +// Tests whether RTCP Common Header validation logic is correct. +TEST(RtcpCommonTest, WillNotParseHeaderWithInvalidData) { + // clang-format off + const uint8_t kCastFeedbackPacket[] = { + 0b10000001, // Version=2, Padding=no, ItemCount=1 byte. + 206, // RTCP Packet type byte. + 0x00, 0x04, // Length of remainder of packet, in 32-bit words. + 9, 8, 7, 6, // SSRC of receiver. + 1, 2, 3, 4, // SSRC of sender. + 'C', 'A', 'S', 'T', + 0x0a, // Checkpoint Frame ID (lower 8 bits). + 0x00, // Number of "Loss Fields" + 0x00, 0x28, // Current Playout Delay in milliseconds. + }; + // clang-format on + + // Start with a valid packet, and expect the parse to succeed. + uint8_t buffer[sizeof(kCastFeedbackPacket)]; + memcpy(buffer, kCastFeedbackPacket, sizeof(buffer)); + EXPECT_TRUE(RtcpCommonHeader::Parse(buffer).has_value()); + + // Wrong version in first byte: Expect parse failure. + buffer[0] = 0b01000001; + EXPECT_FALSE(RtcpCommonHeader::Parse(buffer).has_value()); + buffer[0] = kCastFeedbackPacket[0]; + + // Wrong packet type (not in RTCP range): Expect parse failure. + buffer[1] = 42; + EXPECT_FALSE(RtcpCommonHeader::Parse(buffer).has_value()); + buffer[1] = kCastFeedbackPacket[1]; +} + +// Test that the Report Block optionally included in Sender Reports or Receiver +// Reports can be serialized and re-parsed correctly. +TEST(RtcpCommonTest, SerializesAndParsesRtcpReportBlocks) { + constexpr Ssrc kSsrc{0x04050607}; + + RtcpReportBlock original; + original.ssrc = kSsrc; + original.packet_fraction_lost_numerator = 0x67; + original.cumulative_packets_lost = 74536; + original.extended_high_sequence_number = 0x0201fedc; + original.jitter = RtpTimeDelta::FromTicks(123); + original.last_status_report_id = 0x0908; + original.delay_since_last_report = RtcpReportBlock::Delay(99999); + + uint8_t buffer[kRtcpReportBlockSize]; + SerializeAndExpectPointerAdvanced(original, kRtcpReportBlockSize, buffer); + + // If the number of report blocks is zero, or some other SSRC is specified, + // ParseOne() should not return a result. + EXPECT_FALSE(RtcpReportBlock::ParseOne(buffer, 0, 0).has_value()); + EXPECT_FALSE(RtcpReportBlock::ParseOne(buffer, 0, kSsrc).has_value()); + EXPECT_FALSE(RtcpReportBlock::ParseOne(buffer, 1, 0).has_value()); + + // Expect that the report block is parsed correctly. + const auto parsed = RtcpReportBlock::ParseOne(buffer, 1, kSsrc); + ASSERT_TRUE(parsed.has_value()); + EXPECT_EQ(original.ssrc, parsed->ssrc); + EXPECT_EQ(original.packet_fraction_lost_numerator, + parsed->packet_fraction_lost_numerator); + EXPECT_EQ(original.cumulative_packets_lost, parsed->cumulative_packets_lost); + EXPECT_EQ(original.extended_high_sequence_number, + parsed->extended_high_sequence_number); + EXPECT_EQ(original.jitter, parsed->jitter); + EXPECT_EQ(original.last_status_report_id, parsed->last_status_report_id); + EXPECT_EQ(original.delay_since_last_report, parsed->delay_since_last_report); +} + +// Tests that the Report Block parser can, among multiple Report Blocks, find +// the one with a matching recipient SSRC. +TEST(RtcpCommonTest, ParsesOneReportBlockFromMultipleBlocks) { + constexpr Ssrc kSsrc{0x04050607}; + constexpr int kNumBlocks = 5; + + RtcpReportBlock expected; + expected.ssrc = kSsrc; + expected.packet_fraction_lost_numerator = 0x67; + expected.cumulative_packets_lost = 74536; + expected.extended_high_sequence_number = 0x0201fedc; + expected.jitter = RtpTimeDelta::FromTicks(123); + expected.last_status_report_id = 0x0908; + expected.delay_since_last_report = RtcpReportBlock::Delay(99999); + + // Generate multiple report blocks with different recipient SSRCs. + uint8_t buffer[kRtcpReportBlockSize * kNumBlocks]; + absl::Span<uint8_t> buffer_span(buffer, kRtcpReportBlockSize * kNumBlocks); + for (int i = 0; i < kNumBlocks; ++i) { + RtcpReportBlock another; + another.ssrc = expected.ssrc + i - 2; + another.packet_fraction_lost_numerator = + expected.packet_fraction_lost_numerator + i - 2; + another.cumulative_packets_lost = expected.cumulative_packets_lost + i - 2; + another.extended_high_sequence_number = + expected.extended_high_sequence_number + i - 2; + another.jitter = expected.jitter + RtpTimeDelta::FromTicks(i - 2); + another.last_status_report_id = expected.last_status_report_id + i - 2; + another.delay_since_last_report = + expected.delay_since_last_report + RtcpReportBlock::Delay(i - 2); + + another.AppendFields(&buffer_span); + } + + // Expect that the desired report block is found and parsed correctly. + const auto parsed = RtcpReportBlock::ParseOne(buffer, kNumBlocks, kSsrc); + ASSERT_TRUE(parsed.has_value()); + EXPECT_EQ(expected.ssrc, parsed->ssrc); + EXPECT_EQ(expected.packet_fraction_lost_numerator, + parsed->packet_fraction_lost_numerator); + EXPECT_EQ(expected.cumulative_packets_lost, parsed->cumulative_packets_lost); + EXPECT_EQ(expected.extended_high_sequence_number, + parsed->extended_high_sequence_number); + EXPECT_EQ(expected.jitter, parsed->jitter); + EXPECT_EQ(expected.last_status_report_id, parsed->last_status_report_id); + EXPECT_EQ(expected.delay_since_last_report, parsed->delay_since_last_report); +} + +// Tests the helper for computing the packet fraction loss numerator, a value +// that should always be between 0 and 255, in terms of absolute packet counts. +TEST(RtcpCommonTest, ComputesPacketLossFractionForReportBlocks) { + const auto ComputeFractionLost = [](int64_t num_apparently_sent, + int64_t num_received) { + RtcpReportBlock report; + report.SetPacketFractionLostNumerator(num_apparently_sent, num_received); + return report.packet_fraction_lost_numerator; + }; + + // If no non-duplicate packets were sent to the Receiver, the packet loss + // fraction should be zero. + EXPECT_EQ(0, ComputeFractionLost(0, 0)); + EXPECT_EQ(0, ComputeFractionLost(0, 1)); + EXPECT_EQ(0, ComputeFractionLost(0, 999)); + + // If the same number or more packets were received than those apparently + // sent, the packet loss fraction should be zero. + EXPECT_EQ(0, ComputeFractionLost(1, 1)); + EXPECT_EQ(0, ComputeFractionLost(1, 2)); + EXPECT_EQ(0, ComputeFractionLost(1, 4)); + EXPECT_EQ(0, ComputeFractionLost(4, 5)); + EXPECT_EQ(0, ComputeFractionLost(42, 42)); + EXPECT_EQ(0, ComputeFractionLost(60, 999)); + + // Test various partial loss scenarios. + EXPECT_EQ(85, ComputeFractionLost(3, 2)); + EXPECT_EQ(128, ComputeFractionLost(10, 5)); + EXPECT_EQ(174, ComputeFractionLost(22, 7)); + + // Test various total-loss/near-total-loss scenarios. + EXPECT_EQ(255, ComputeFractionLost(17, 0)); + EXPECT_EQ(255, ComputeFractionLost(100, 0)); + EXPECT_EQ(255, ComputeFractionLost(9876, 1)); +} + +// Tests the helper for computing the cumulative packet loss total, a value that +// should always be between 0 and 2^24 - 1, in terms of absolute packet counts. +TEST(RtcpCommonTest, ComputesCumulativePacketLossForReportBlocks) { + const auto ComputeLoss = [](int64_t num_apparently_sent, + int64_t num_received) { + RtcpReportBlock report; + report.SetCumulativePacketsLost(num_apparently_sent, num_received); + return report.cumulative_packets_lost; + }; + + // Test various no-loss scenarios (including duplicate packets). + EXPECT_EQ(0, ComputeLoss(0, 0)); + EXPECT_EQ(0, ComputeLoss(0, 1)); + EXPECT_EQ(0, ComputeLoss(3, 3)); + EXPECT_EQ(0, ComputeLoss(56, 56)); + EXPECT_EQ(0, ComputeLoss(std::numeric_limits<int64_t>::max() - 12, + std::numeric_limits<int64_t>::max())); + EXPECT_EQ(0, ComputeLoss(std::numeric_limits<int64_t>::max(), + std::numeric_limits<int64_t>::max())); + + // Test various partial loss scenarios. + EXPECT_EQ(1, ComputeLoss(2, 1)); + EXPECT_EQ(2, ComputeLoss(42, 40)); + EXPECT_EQ(1025, ComputeLoss(999999, 999999 - 1025)); + EXPECT_EQ(1, ComputeLoss(std::numeric_limits<int64_t>::max(), + std::numeric_limits<int64_t>::max() - 1)); + + // Test that a huge cumulative loss saturates to the maximum valid value for + // the field. + EXPECT_EQ((1 << 24) - 1, ComputeLoss(999999999, 1)); +} + +// Tests the helper that converts Clock::durations to the report blocks timebase +// (1/65536 sconds), and also that it saturates to to the valid range of values +// (0 to 2^32 - 1 ticks). +TEST(RtcpCommonTest, ComputesDelayForReportBlocks) { + RtcpReportBlock report; + using Delay = RtcpReportBlock::Delay; + + const auto ComputeDelay = [](Clock::duration delay_in_wrong_timebase) { + RtcpReportBlock report; + report.SetDelaySinceLastReport(delay_in_wrong_timebase); + return report.delay_since_last_report; + }; + + // A duration less than or equal to zero should clamp to zero. + EXPECT_EQ(Delay::zero(), ComputeDelay(Clock::duration::min())); + EXPECT_EQ(Delay::zero(), ComputeDelay(std::chrono::milliseconds(-1234))); + EXPECT_EQ(Delay::zero(), ComputeDelay(Clock::duration::zero())); + + // Test conversion of various durations that should not clamp. + EXPECT_EQ(Delay(32768 /* 1/2 second worth of ticks */), + ComputeDelay(std::chrono::milliseconds(500))); + EXPECT_EQ(Delay(65536 /* 1 second worth of ticks */), + ComputeDelay(std::chrono::seconds(1))); + EXPECT_EQ(Delay(655360 /* 10 seconds worth of ticks */), + ComputeDelay(std::chrono::seconds(10))); + EXPECT_EQ(Delay(4294967294), + ComputeDelay(std::chrono::microseconds(65535999983))); + EXPECT_EQ(Delay(4294967294), + ComputeDelay(std::chrono::microseconds(65535999984))); + + // A too-large duration should clamp to the maximum-possible Delay value. + EXPECT_EQ(Delay(4294967295), + ComputeDelay(std::chrono::microseconds(65535999985))); + EXPECT_EQ(Delay(4294967295), + ComputeDelay(std::chrono::microseconds(65535999986))); + EXPECT_EQ(Delay(4294967295), + ComputeDelay(std::chrono::microseconds(999999000000))); + EXPECT_EQ(Delay(4294967295), ComputeDelay(Clock::duration::max())); +} + +} // namespace +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/rtcp_session.cc b/cast/streaming/rtcp_session.cc new file mode 100644 index 00000000..a8b8ab6a --- /dev/null +++ b/cast/streaming/rtcp_session.cc @@ -0,0 +1,26 @@ +// 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/rtcp_session.h" + +#include "util/logging.h" + +namespace cast { +namespace streaming { + +RtcpSession::RtcpSession(Ssrc sender_ssrc, + Ssrc receiver_ssrc, + openscreen::platform::Clock::time_point start_time) + : sender_ssrc_(sender_ssrc), + receiver_ssrc_(receiver_ssrc), + ntp_converter_(start_time) { + OSP_DCHECK_NE(sender_ssrc_, kNullSsrc); + OSP_DCHECK_NE(receiver_ssrc_, kNullSsrc); + OSP_DCHECK_NE(sender_ssrc_, receiver_ssrc_); +} + +RtcpSession::~RtcpSession() = default; + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/rtcp_session.h b/cast/streaming/rtcp_session.h new file mode 100644 index 00000000..d896cb94 --- /dev/null +++ b/cast/streaming/rtcp_session.h @@ -0,0 +1,42 @@ +// 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. + +#ifndef CAST_STREAMING_RTCP_SESSION_H_ +#define CAST_STREAMING_RTCP_SESSION_H_ + +#include "cast/streaming/ntp_time.h" +#include "cast/streaming/ssrc.h" + +namespace cast { +namespace streaming { + +// Session-level configuration and shared components for the RTCP messaging +// associated with a single Cast RTP stream. Multiple packet serialization and +// parsing components share a single RtcpSession instance for data consistency. +class RtcpSession { + public: + // |start_time| should be the current time, as it is used by NtpTimeConverter + // to set a fixed reference point between the local Clock and current "real + // world" wall time. + RtcpSession(Ssrc sender_ssrc, + Ssrc receiver_ssrc, + openscreen::platform::Clock::time_point start_time); + ~RtcpSession(); + + Ssrc sender_ssrc() const { return sender_ssrc_; } + Ssrc receiver_ssrc() const { return receiver_ssrc_; } + const NtpTimeConverter& ntp_converter() const { return ntp_converter_; } + + private: + const Ssrc sender_ssrc_; + const Ssrc receiver_ssrc_; + + // Translates between system time (internal format) and NTP (wire format). + NtpTimeConverter ntp_converter_; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_RTCP_SESSION_H_ diff --git a/cast/streaming/rtp_defines.cc b/cast/streaming/rtp_defines.cc new file mode 100644 index 00000000..0c359129 --- /dev/null +++ b/cast/streaming/rtp_defines.cc @@ -0,0 +1,46 @@ +// 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/rtp_defines.h" + +namespace cast { +namespace streaming { + +bool IsRtpPayloadType(uint8_t raw_byte) { + switch (static_cast<RtpPayloadType>(raw_byte)) { + case RtpPayloadType::kAudioOpus: + case RtpPayloadType::kAudioAac: + case RtpPayloadType::kAudioPcm16: + case RtpPayloadType::kAudioVarious: + case RtpPayloadType::kVideoVp8: + case RtpPayloadType::kVideoH264: + case RtpPayloadType::kVideoVarious: + case RtpPayloadType::kAudioHackForAndroidTV: + // Note: RtpPayloadType::kVideoHackForAndroidTV has the same value as + // kAudioOpus. + return true; + + case RtpPayloadType::kNull: + break; + } + return false; +} + +bool IsRtcpPacketType(uint8_t raw_byte) { + switch (static_cast<RtcpPacketType>(raw_byte)) { + case RtcpPacketType::kSenderReport: + case RtcpPacketType::kReceiverReport: + case RtcpPacketType::kApplicationDefined: + case RtcpPacketType::kPayloadSpecific: + case RtcpPacketType::kExtendedReports: + return true; + + case RtcpPacketType::kNull: + break; + } + return false; +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/rtp_defines.h b/cast/streaming/rtp_defines.h new file mode 100644 index 00000000..94294cd6 --- /dev/null +++ b/cast/streaming/rtp_defines.h @@ -0,0 +1,344 @@ +// 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. + +#ifndef CAST_STREAMING_RTP_DEFINES_H_ +#define CAST_STREAMING_RTP_DEFINES_H_ + +#include <stdint.h> + +namespace cast { +namespace streaming { + +// Note: Cast Streaming uses a subset of the messages in the RTP/RTCP +// specification, but also adds some of its own extensions. See: +// https://tools.ietf.org/html/rfc3550 + +// Uniquely identifies one packet within a frame. These are sequence numbers, +// starting at 0. Each Cast RTP packet also includes the "last ID" so that a +// receiver always knows the range of valid FramePacketIds for a given frame. +using FramePacketId = uint16_t; + +// A special FramePacketId value meant to represent "all packets lost" in Cast +// RTCP Feedback messages. +constexpr FramePacketId kAllPacketsLost = 0xffff; +constexpr FramePacketId kMaxAllowedFramePacketId = kAllPacketsLost - 1; + +// The maximum size of any RTP or RTCP packet, in bytes. The calculation below +// is: Standard Ethernet MTU bytes minus IP header bytes minus UDP header bytes. +// The remainder is available for RTP/RTCP packet data (header + payload). +// +// A nice explanation of this: https://jvns.ca/blog/2017/02/07/mtu/ +// +// Constants are provided here for UDP over IPv4 and IPv6 on Ethernet. Other +// transports and network mediums will need additional consideration, alternate +// calculations. Note that MTU is dynamic, depending on the path the packets +// take between two endpoints (the 1500 here is just a commonly-used value for +// LAN Ethernet). +constexpr int kMaxRtpPacketSizeForIpv4UdpOnEthernet = 1500 - 20 - 8; +constexpr int kMaxRtpPacketSizeForIpv6UdpOnEthernet = 1500 - 40 - 8; + +// The Cast RTP packet header: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ^ +// |V=2|P|X| CC=0 |M| PT | sequence number | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+RTP +// + RTP timestamp |Spec +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +// + synchronization source (SSRC) identifier | v +// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ +// |K|R| EXT count | FID | PID | ^ +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+Cast +// | Max PID | optional fields, extensions, Spec +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ then payload... v +// +// Byte 0: Version 2, no padding, no RTP extensions, no CSRCs. +// Byte 1: Marker bit indicates whether this is the last packet, followed by a +// 7-bit payload type. +// Byte 12: Key Frame bit, followed by "RFID will be provided" bit, followed by +// 6 bits specifying the number of extensions that will be provided. + +// The minimum-possible valid size of a Cast RTP packet (i.e., no optional +// fields, extensions, nor payload). +constexpr int kRtpPacketMinValidSize = 18; + +// All Cast RTP packets must carry the version 2 flag, not use padding, not use +// RTP extensions, and have zero CSRCs. +constexpr uint8_t kRtpRequiredFirstByte = 0b10000000; + +// Bitmasks to isolate fields within byte 2 of the Cast RTP header. +constexpr uint8_t kRtpMarkerBitMask = 0b10000000; +constexpr uint8_t kRtpPayloadTypeMask = 0b01111111; + +// Describes the content being transported over RTP streams. These are Cast +// Streaming specific assignments, within the "dynamic" range provided by +// IANA. Note that this Cast Streaming implementation does not manipulate +// already-encoded data, and so these payload types are only "informative" in +// purpose and can be used to check for corruption while parsing packets. +enum class RtpPayloadType : uint8_t { + kNull = 0, + + kAudioFirst = 96, + kAudioOpus = 96, + kAudioAac = 97, + kAudioPcm16 = 98, + kAudioVarious = 99, // Codec being used is not fixed. + + kVideoFirst = 100, + kVideoVp8 = 100, + kVideoH264 = 101, + kVideoVarious = 102, // Codec being used is not fixed. + + // Some AndroidTV receivers require the payload type for audio to be 127, and + // video to be 96; regardless of the codecs actually being used. This is + // definitely out-of-spec, and inconsistent with the audio versus video range + // of values, but must be taken into account for backwards-compatibility. + kAudioHackForAndroidTV = 127, + kVideoHackForAndroidTV = 96, +}; + +// Returns true if the |raw_byte| can be type-casted to a RtpPayloadType, and is +// also not RtpPayloadType::kNull. The caller should mask the byte, to select +// the lower 7 bits, if applicable. +bool IsRtpPayloadType(uint8_t raw_byte); + +// Bitmasks to isolate fields within byte 12 of the Cast RTP header. +constexpr uint8_t kRtpKeyFrameBitMask = 0b10000000; +constexpr uint8_t kRtpHasReferenceFrameIdBitMask = 0b01000000; +constexpr uint8_t kRtpExtensionCountMask = 0b00111111; + +// Cast extensions. This implementation supports only the Adaptive Latency +// extension, and ignores all others: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | TYPE = 1 | Ext data SIZE = 2 |Playout Delay (unsigned millis)| +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +// The Adaptive Latency extension permits changing the fixed end-to-end playout +// delay of a single RTP stream. +constexpr uint8_t kAdaptiveLatencyRtpExtensionType = 1; +constexpr int kNumExtensionDataSizeFieldBits = 10; + +// RTCP Common Header: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |V=2|P|RC/Subtyp| Packet Type | Length | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +constexpr int kRtcpCommonHeaderSize = 4; +// All RTCP packets must carry the version 2 flag and not use padding. +constexpr uint8_t kRtcpRequiredVersionAndPaddingBits = 0b100; +constexpr int kRtcpReportCountFieldNumBits = 5; + +// https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml +enum class RtcpPacketType : uint8_t { + kNull = 0, + + kSenderReport = 200, + kReceiverReport = 201, + kApplicationDefined = 204, + kPayloadSpecific = 206, + kExtendedReports = 207, +}; + +// Returns true if the |raw_byte| can be type-casted to a RtcpPacketType, and is +// also not RtcpPacketType::kNull. +bool IsRtcpPacketType(uint8_t raw_byte); + +// Supported subtype values in the RTCP Common Header when the packet type is +// kApplicationDefined or kPayloadSpecific. +enum class RtcpSubtype : uint8_t { + kNull = 0, + + kPictureLossIndicator = 1, + kReceiverLog = 2, + kFeedback = 15, +}; + +// RTCP Sender Report: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | SSRC of Sender | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | | +// | NTP Timestamp | +// | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | RTP Timestamp | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Sender's Packet Count | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Sender's Octet Count | +// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ +// ...Followed by zero or more "Report Blocks"... +constexpr int kRtcpSenderReportSize = 24; + +// RTCP Receiver Report: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | SSRC of Receiver | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// ...Followed by zero or more "Report Blocks"... +constexpr int kRtcpReceiverReportSize = 4; + +// RTCP Report Block. For Cast Streaming, zero or one of these accompanies a +// Sender or Receiver Report, which is different than the RTCP spec (which +// allows zero or more). +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | "To" SSRC | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Fraction Lost | Cumulative Number of Packets Lost | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | [32-bit extended] Highest Sequence Number Received | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Interarrival Jitter Mean Absolute Deviation (in RTP Timebase) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Middle 32-bits of NTP Timestamp from last Sender Report | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Delay since last Sender Report (1/65536 sec timebase) | +// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ +constexpr int kRtcpReportBlockSize = 24; +constexpr int kRtcpCumulativePacketsFieldNumBits = 24; + +// Cast Feedback Message: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | SSRC of Receiver | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | SSRC of Sender | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Unique identifier 'C' 'A' 'S' 'T' | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | CkPt Frame ID | # Loss Fields | Current Playout Delay (msec) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +constexpr int kRtcpFeedbackHeaderSize = 16; +constexpr uint32_t kRtcpCastIdentifierWord = + (uint32_t{'C'} << 24) | (uint32_t{'A'} << 16) | (uint32_t{'S'} << 8) | + uint32_t{'T'}; +// +// "Checkpoint Frame ID" indicates that all frames prior to and including this +// one have been fully received. Unfortunately, the Frame ID is truncated to its +// lower 8 bits in the packet, and 8 bits is not really enough: If a RTCP packet +// is received very late (e.g., more than 1.2 seconds late for 100 FPS audio), +// the Checkpoint Frame ID here will be mis-interpreted as representing a +// higher-numbered frame than what was intended. This could make the sender's +// tracking of "completely received" frames inconsistent, and Cast Streaming +// would live-lock. However, this design issue has been baked into the spec and +// millions of deployments over several years, and so there's no changing it +// now. See kMaxUnackedFrames in constants.h. +// +// "# Loss fields" indicates the number of packet-level NACK words, 0 to 255: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | w/in Frame ID | Lost Frame Packet ID | PID BitVector | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +constexpr int kRtcpFeedbackLossFieldSize = 4; +// +// "Within Frame ID" is a truncated-to-8-bits frame ID field and, when +// bit-expanded should always be interpreted to represent a value greater than +// the Checkpoint Frame ID. "Lost Frame Packet ID" is either a specific packet +// (within the frame) that has not been received, or kAllPacketsLost to indicate +// none the packets for the frame have been received yet. In the former case, +// "PID Bit Vector" then represents which of the next 8 packets are also +// missing. +// +// Finally, all of the above is optionally followed by a frame-level ACK bit +// vector: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Unique identifier 'C' 'S' 'T' '2' | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |Feedback Count | # BVectOctets | ACK BitVect (2 to 254 bytes)... +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ → zero-padded to word boundary +constexpr int kRtcpFeedbackAckHeaderSize = 6; +constexpr uint32_t kRtcpCst2IdentifierWord = + (uint32_t{'C'} << 24) | (uint32_t{'S'} << 16) | (uint32_t{'T'} << 8) | + uint32_t{'2'}; +constexpr int kRtcpMinAckBitVectorOctets = 2; +constexpr int kRtcpMaxAckBitVectorOctets = 254; +// +// "Feedback Count" is a wrap-around counter indicating the number of Cast +// Feedbacks that have been sent before this one. "# Bit Vector Octets" +// indicates the number of bytes of ACK bit vector following. Cast RTCP +// alignment/padding requirements (to 4-byte boundaries) dictates the following +// rules for generating the ACK bit vector: +// +// 1. There must be at least 2 bytes of ACK bit vector, if only to pad the 6 +// byte header with two more bytes. +// 2. If more than 2 bytes are needed, they must be added 4 at a time to +// maintain the 4-byte alignment of the overall RTCP packet. +// 3. The total number of octets may not exceed 255; but, because of #2, 254 +// is effectively the limit. +// 4. The first bit in the first octet represents "Checkpoint Frame ID" plus +// two. "Plus two" and not "plus one" because otherwise the "Checkpoint +// Frame ID" should have been a greater value! + +// RTCP Extended Report: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | SSRC of Report Author | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +constexpr int kRtcpExtendedReportHeaderSize = 4; +// +// ...followed by zero or more Blocks: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Block Type | Reserved = 0 | Block Length | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | ..."Block Length" words of report data... | +// + + +// + + +constexpr int kRtcpExtendedReportBlockHeaderSize = 4; +// +// Cast Streaming only uses Receiver Reference Time Reports: +// https://tools.ietf.org/html/rfc3611#section-4.4. So, the entire block would +// be: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Block Type=4 | Reserved = 0 | Block Length = 2 | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | NTP Timestamp | +// | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +constexpr uint8_t kRtcpReceiverReferenceTimeReportBlockType = 4; +constexpr int kRtcpReceiverReferenceTimeReportBlockSize = 8; + +// Cast Picture Loss Indicator Message: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | SSRC of Receiver | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | SSRC of Sender | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +constexpr int kRtcpPictureLossIndicatorHeaderSize = 8; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_RTP_DEFINES_H_ diff --git a/cast/streaming/rtp_packet_parser.cc b/cast/streaming/rtp_packet_parser.cc new file mode 100644 index 00000000..5fbf005a --- /dev/null +++ b/cast/streaming/rtp_packet_parser.cc @@ -0,0 +1,118 @@ +// 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/rtp_packet_parser.h" + +#include <algorithm> +#include <utility> + +#include "cast/streaming/packet_util.h" +#include "util/logging.h" + +using openscreen::ReadBigEndian; + +namespace cast { +namespace streaming { + +RtpPacketParser::RtpPacketParser(Ssrc sender_ssrc) + : sender_ssrc_(sender_ssrc), highest_rtp_frame_id_(FrameId::first()) {} + +RtpPacketParser::~RtpPacketParser() = default; + +absl::optional<RtpPacketParser::ParseResult> RtpPacketParser::Parse( + absl::Span<const uint8_t> buffer) { + if (buffer.size() < kRtpPacketMinValidSize || + ConsumeField<uint8_t>(&buffer) != kRtpRequiredFirstByte) { + return absl::nullopt; + } + + // RTP header elements. + // + // Note: M (marker bit) is ignored here. Technically, according to the Cast + // Streaming spec, it should only be set when PID == Max PID; but, let's be + // lenient just in case some sender implementations don't adhere to this tiny, + // subtle detail. + const uint8_t payload_type = + ConsumeField<uint8_t>(&buffer) & kRtpPayloadTypeMask; + if (!IsRtpPayloadType(payload_type)) { + return absl::nullopt; + } + ParseResult result; + result.payload_type = static_cast<RtpPayloadType>(payload_type); + result.sequence_number = ConsumeField<uint16_t>(&buffer); + result.rtp_timestamp = + last_parsed_rtp_timestamp_.Expand(ConsumeField<uint32_t>(&buffer)); + if (ConsumeField<uint32_t>(&buffer) != sender_ssrc_) { + return absl::nullopt; + } + + // Cast-specific header elements. + const uint8_t byte12 = ConsumeField<uint8_t>(&buffer); + result.is_key_frame = !!(byte12 & kRtpKeyFrameBitMask); + const bool has_referenced_frame_id = + !!(byte12 & kRtpHasReferenceFrameIdBitMask); + const size_t num_cast_extensions = byte12 & kRtpExtensionCountMask; + result.frame_id = + highest_rtp_frame_id_.Expand(ConsumeField<uint8_t>(&buffer)); + result.packet_id = ConsumeField<uint16_t>(&buffer); + result.max_packet_id = ConsumeField<uint16_t>(&buffer); + if (result.max_packet_id == kAllPacketsLost) { + return absl::nullopt; // Packet ID cannot be the special value. + } + if (result.packet_id > result.max_packet_id) { + return absl::nullopt; + } + if (has_referenced_frame_id) { + if (buffer.empty()) { + return absl::nullopt; + } + result.referenced_frame_id = + result.frame_id.Expand(ConsumeField<uint8_t>(&buffer)); + } else { + // By default, if no reference frame ID was provided, the assumption is that + // a key frame only references itself, while non-key frames reference only + // their immediate predecessor. + result.referenced_frame_id = + result.is_key_frame ? result.frame_id : (result.frame_id - 1); + } + + // Zero or more Cast extensions. + for (size_t i = 0; i < num_cast_extensions; ++i) { + if (buffer.size() < sizeof(uint16_t)) { + return absl::nullopt; + } + const uint16_t type_and_size = ConsumeField<uint16_t>(&buffer); + const uint8_t type = type_and_size >> kNumExtensionDataSizeFieldBits; + const size_t size = + type_and_size & FieldBitmask<uint16_t>(kNumExtensionDataSizeFieldBits); + if (buffer.size() < size) { + return absl::nullopt; + } + if (type == kAdaptiveLatencyRtpExtensionType) { + if (size != sizeof(uint16_t)) { + return absl::nullopt; + } + result.new_playout_delay = + std::chrono::milliseconds(ReadBigEndian<uint16_t>(buffer.data())); + } + buffer.remove_prefix(size); + } + + // All remaining data in the packet is the payload. + result.payload = buffer; + + // At this point, the packet is known to be well-formed. Track recent field + // values for later parses, to bit-extend the truncated values found in future + // packets. + last_parsed_rtp_timestamp_ = result.rtp_timestamp; + highest_rtp_frame_id_ = std::max(highest_rtp_frame_id_, result.frame_id); + + return result; +} + +RtpPacketParser::ParseResult::ParseResult() = default; +RtpPacketParser::ParseResult::~ParseResult() = default; + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/rtp_packet_parser.h b/cast/streaming/rtp_packet_parser.h new file mode 100644 index 00000000..b2be4c53 --- /dev/null +++ b/cast/streaming/rtp_packet_parser.h @@ -0,0 +1,78 @@ +// 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. + +#ifndef CAST_STREAMING_RTP_PACKET_PARSER_H_ +#define CAST_STREAMING_RTP_PACKET_PARSER_H_ + +#include <chrono> + +#include "absl/types/optional.h" +#include "absl/types/span.h" +#include "cast/streaming/frame_id.h" +#include "cast/streaming/rtp_defines.h" +#include "cast/streaming/rtp_time.h" +#include "cast/streaming/ssrc.h" + +namespace cast { +namespace streaming { + +// Parses RTP packets for all frames in the same Cast RTP stream. One +// RtpPacketParser instance should be used for all RTP packets having the same +// SSRC. +// +// Note that the parser is not stateless: One of its responsibilities is to +// bit-expand values that exist in a truncated form within the packets. It +// tracks the progression of those values in a live system to re-constitute such +// values. +class RtpPacketParser { + public: + struct ParseResult { + // Elements from RTP packet header. + // https://tools.ietf.org/html/rfc3550#section-5 + RtpPayloadType payload_type; + uint16_t sequence_number; // Wrap-around packet transmission counter. + RtpTimeTicks rtp_timestamp; // The media timestamp. + + // Elements from Cast header (at beginning of RTP payload). + bool is_key_frame; + FrameId frame_id; + FramePacketId packet_id; // Always in the range [0,max_packet_id]. + FramePacketId max_packet_id; + FrameId referenced_frame_id; // ID of frame required to decode this one. + std::chrono::milliseconds new_playout_delay{}; // Ignore if non-positive. + + // Portion of the |packet| that was passed into Parse() that contains the + // payload. WARNING: This memory region is only valid while the original + // |packet| memory remains valid. + absl::Span<const uint8_t> payload; + + ParseResult(); + ~ParseResult(); + }; + + explicit RtpPacketParser(Ssrc sender_ssrc); + ~RtpPacketParser(); + + // Parses the packet. The caller should use InspectPacketForRouting() + // beforehand to ensure that the packet is meant to be parsed by this + // instance. Returns absl::nullopt if the |packet| was corrupt. + absl::optional<ParseResult> Parse(absl::Span<const uint8_t> packet); + + private: + const Ssrc sender_ssrc_; + + // Tracks recently-parsed RTP timestamps so that the truncated values can be + // re-expanded into full-form. + RtpTimeTicks last_parsed_rtp_timestamp_; + + // The highest frame ID seen in any RTP packets so far. This is tracked so + // that the truncated frame ID fields in RTP packets can be re-expanded into + // full-form. + FrameId highest_rtp_frame_id_; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_RTP_PACKET_PARSER_H_ diff --git a/cast/streaming/rtp_packet_parser_fuzzer.cc b/cast/streaming/rtp_packet_parser_fuzzer.cc new file mode 100644 index 00000000..24169ce7 --- /dev/null +++ b/cast/streaming/rtp_packet_parser_fuzzer.cc @@ -0,0 +1,32 @@ +// 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 <stdint.h> + +#include "cast/streaming/rtp_packet_parser.h" + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + using cast::streaming::RtpPacketParser; + using cast::streaming::Ssrc; + + constexpr Ssrc kSenderSsrcInSeedCorpus = 0x01020304; + RtpPacketParser parser(kSenderSsrcInSeedCorpus); + parser.Parse(absl::Span<const uint8_t>(data, size)); + + return 0; +} + +#if defined(NEEDS_MAIN_TO_CALL_FUZZER_DRIVER) + +// Forward declarations of Clang's built-in libFuzzer driver. +namespace fuzzer { +using TestOneInputCallback = int (*)(const uint8_t* data, size_t size); +int FuzzerDriver(int* argc, char*** argv, TestOneInputCallback callback); +} // namespace fuzzer + +int main(int argc, char* argv[]) { + return fuzzer::FuzzerDriver(&argc, &argv, LLVMFuzzerTestOneInput); +} + +#endif // defined(NEEDS_MAIN_TO_CALL_FUZZER_DRIVER) diff --git a/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame.bin b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame.bin new file mode 100644 index 00000000..def766ea --- /dev/null +++ b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame.bin @@ -0,0 +1,4 @@ +€`¾ï € + +
+
\ No newline at end of file diff --git a/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame_with_bad_packet_id.bin b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame_with_bad_packet_id.bin Binary files differnew file mode 100644 index 00000000..8585eeb1 --- /dev/null +++ b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame_with_bad_packet_id.bin diff --git a/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame_with_latency_ext.bin b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame_with_latency_ext.bin Binary files differnew file mode 100644 index 00000000..07cd9f8e --- /dev/null +++ b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame_with_latency_ext.bin diff --git a/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame_with_multiple_ext.bin b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame_with_multiple_ext.bin Binary files differnew file mode 100644 index 00000000..16d39eb4 --- /dev/null +++ b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame_with_multiple_ext.bin diff --git a/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_non_key_frame_with_rfid.bin b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_non_key_frame_with_rfid.bin Binary files differnew file mode 100644 index 00000000..5aeb2a0c --- /dev/null +++ b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_non_key_frame_with_rfid.bin diff --git a/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_non_key_frame_without_rfid.bin b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_non_key_frame_without_rfid.bin Binary files differnew file mode 100644 index 00000000..9b1fdc4d --- /dev/null +++ b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_non_key_frame_without_rfid.bin diff --git a/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_18_bytes.bin b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_18_bytes.bin Binary files differnew file mode 100644 index 00000000..6b40319f --- /dev/null +++ b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_18_bytes.bin diff --git a/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_1_byte.bin b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_1_byte.bin new file mode 100644 index 00000000..5416677b --- /dev/null +++ b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_1_byte.bin @@ -0,0 +1 @@ +€
\ No newline at end of file diff --git a/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_22_bytes.bin b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_22_bytes.bin Binary files differnew file mode 100644 index 00000000..e6bff22e --- /dev/null +++ b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_22_bytes.bin diff --git a/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_33_bytes.bin b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_33_bytes.bin Binary files differnew file mode 100644 index 00000000..a50280d6 --- /dev/null +++ b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_33_bytes.bin diff --git a/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_34_bytes.bin b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_34_bytes.bin Binary files differnew file mode 100644 index 00000000..66364dae --- /dev/null +++ b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_34_bytes.bin diff --git a/cast/streaming/rtp_packet_parser_unittest.cc b/cast/streaming/rtp_packet_parser_unittest.cc new file mode 100644 index 00000000..927521c0 --- /dev/null +++ b/cast/streaming/rtp_packet_parser_unittest.cc @@ -0,0 +1,313 @@ +// 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/rtp_packet_parser.h" + +#include "cast/streaming/rtp_defines.h" +#include "gtest/gtest.h" +#include "util/big_endian.h" + +using openscreen::ReadBigEndian; +using openscreen::WriteBigEndian; + +namespace cast { +namespace streaming { +namespace { + +// Tests that a simple packet for a key frame can be parsed. +TEST(RtpPacketParserTest, ParsesPacketForKeyFrame) { + // clang-format off + const uint8_t kInput[] = { + 0b10000000, // Version/Padding byte. + 96, // Payload type byte. + 0xbe, 0xef, // Sequence number. + 9, 8, 7, 6, // RTP timestamp. + 1, 2, 3, 4, // SSRC. + 0b10000000, // Is key frame, no extensions. + 5, // Frame ID. + 0xa, 0xb, // Packet ID. + 0xa, 0xc, // Max packet ID. + 0xf, 0xe, 0xd, 0xc, 0xb, 0xa, 0x9, 0x8, // Payload. + }; + // clang-format on + const Ssrc kSenderSsrc = 0x01020304; + + RtpPacketParser parser(kSenderSsrc); + const auto result = parser.Parse(kInput); + ASSERT_TRUE(result); + EXPECT_EQ(RtpPayloadType::kAudioOpus, result->payload_type); + EXPECT_EQ(UINT16_C(0xbeef), result->sequence_number); + EXPECT_EQ(RtpTimeTicks() + RtpTimeDelta::FromTicks(0x09080706), + result->rtp_timestamp); + EXPECT_TRUE(result->is_key_frame); + EXPECT_EQ(FrameId::first() + 5, result->frame_id); + EXPECT_EQ(FramePacketId{0x0a0b}, result->packet_id); + EXPECT_EQ(FramePacketId{0x0a0c}, result->max_packet_id); + EXPECT_EQ(FrameId::first() + 5, result->referenced_frame_id); + EXPECT_EQ(0, result->new_playout_delay.count()); + const absl::Span<const uint8_t> expected_payload(kInput + 18, 8); + ASSERT_EQ(expected_payload, result->payload); + EXPECT_TRUE(expected_payload == result->payload); +} + +// Tests that a packet which includes a "referenced frame ID" can be parsed. +TEST(RtpPacketParserTest, ParsesPacketForNonKeyFrameWithReferenceFrameId) { + // clang-format off + const uint8_t kInput[] = { + 0b10000000, // Version/Padding byte. + 96, // Payload type byte. + 0xde, 0xad, // Sequence number. + 2, 4, 6, 8, // RTP timestamp. + 0, 0, 1, 1, // SSRC. + 0b01000000, // Not a key frame, but has ref frame ID; no extensions. + 42, // Frame ID. + 0x0, 0xb, // Packet ID. + 0x0, 0xc, // Max packet ID. + 39, // Reference Frame ID. + 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29 // Payload. + }; + // clang-format on + const Ssrc kSenderSsrc = 0x00000101; + + RtpPacketParser parser(kSenderSsrc); + const auto result = parser.Parse(kInput); + ASSERT_TRUE(result); + EXPECT_EQ(RtpPayloadType::kAudioOpus, result->payload_type); + EXPECT_EQ(UINT16_C(0xdead), result->sequence_number); + EXPECT_EQ(RtpTimeTicks() + RtpTimeDelta::FromTicks(0x02040608), + result->rtp_timestamp); + EXPECT_FALSE(result->is_key_frame); + EXPECT_EQ(FrameId::first() + 42, result->frame_id); + EXPECT_EQ(FramePacketId{0x000b}, result->packet_id); + EXPECT_EQ(FramePacketId{0x000c}, result->max_packet_id); + EXPECT_EQ(FrameId::first() + 39, result->referenced_frame_id); + EXPECT_EQ(0, result->new_playout_delay.count()); + const absl::Span<const uint8_t> expected_payload(kInput + 19, 15); + ASSERT_EQ(expected_payload, result->payload); + EXPECT_TRUE(expected_payload == result->payload); +} + +// Tests that a packet which lacks a "referenced frame ID" field can be parsed, +// but the parser will provide the implied referenced_frame_id value in the +// result. +TEST(RtpPacketParserTest, ParsesPacketForNonKeyFrameWithoutReferenceFrameId) { + // clang-format off + const uint8_t kInput[] = { + 0b10000000, // Version/Padding byte. + 96, // Payload type byte. + 0xde, 0xad, // Sequence number. + 2, 4, 6, 8, // RTP timestamp. + 0, 0, 1, 1, // SSRC. + 0b00000000, // Not a key frame, no ref frame ID; no extensions. + 42, // Frame ID. + 0x0, 0xb, // Packet ID. + 0x0, 0xc, // Max packet ID. + 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29 // Payload. + }; + // clang-format on + const Ssrc kSenderSsrc = 0x00000101; + + RtpPacketParser parser(kSenderSsrc); + const auto result = parser.Parse(kInput); + ASSERT_TRUE(result); + EXPECT_EQ(RtpPayloadType::kAudioOpus, result->payload_type); + EXPECT_EQ(UINT16_C(0xdead), result->sequence_number); + EXPECT_EQ(RtpTimeTicks() + RtpTimeDelta::FromTicks(0x02040608), + result->rtp_timestamp); + EXPECT_FALSE(result->is_key_frame); + EXPECT_EQ(FrameId::first() + 42, result->frame_id); + EXPECT_EQ(FramePacketId{0x000b}, result->packet_id); + EXPECT_EQ(FramePacketId{0x000c}, result->max_packet_id); + EXPECT_EQ(FrameId::first() + 41, result->referenced_frame_id); + EXPECT_EQ(0, result->new_playout_delay.count()); + const absl::Span<const uint8_t> expected_payload(kInput + 18, 15); + ASSERT_EQ(expected_payload, result->payload); + EXPECT_TRUE(expected_payload == result->payload); +} + +// Tests that a packet indicating a new playout delay can be parsed. +TEST(RtpPacketParserTest, ParsesPacketWithAdaptiveLatencyExtension) { + // clang-format off + const uint8_t kInput[] = { + 0b10000000, // Version/Padding byte. + 96, // Payload type byte. + 0xde, 0xad, // Sequence number. + 2, 4, 6, 8, // RTP timestamp. + 0, 0, 1, 1, // SSRC. + 0b11000001, // Is key frame, has ref frame ID; has one extension. + 64, // Frame ID. + 0x0, 0x0, // Packet ID. + 0x0, 0xc, // Max packet ID. + 64, // Reference Frame ID. + 4, 2, 1, 14, // Cast Adaptive Latency Extension data. + 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29 // Payload. + }; + // clang-format on + const Ssrc kSenderSsrc = 0x00000101; + + RtpPacketParser parser(kSenderSsrc); + const auto result = parser.Parse(kInput); + ASSERT_TRUE(result); + EXPECT_EQ(RtpPayloadType::kAudioOpus, result->payload_type); + EXPECT_EQ(UINT16_C(0xdead), result->sequence_number); + EXPECT_EQ(RtpTimeTicks() + RtpTimeDelta::FromTicks(0x02040608), + result->rtp_timestamp); + EXPECT_TRUE(result->is_key_frame); + EXPECT_EQ(FrameId::first() + 64, result->frame_id); + EXPECT_EQ(FramePacketId{0x0000}, result->packet_id); + EXPECT_EQ(FramePacketId{0x000c}, result->max_packet_id); + EXPECT_EQ(FrameId::first() + 64, result->referenced_frame_id); + EXPECT_EQ(270, result->new_playout_delay.count()); + const absl::Span<const uint8_t> expected_payload(kInput + 23, 15); + ASSERT_EQ(expected_payload, result->payload); + EXPECT_TRUE(expected_payload == result->payload); +} + +// Tests that the parser can handle multiple Cast Header Extensions in a RTP +// packet, and ignores all but the one (Adaptive Latency) that it understands. +TEST(RtpPacketParserTest, ParsesPacketWithMultipleExtensions) { + // clang-format off + const uint8_t kInput[] = { + 0b10000000, // Version/Padding byte. + 96, // Payload type byte. + 0xde, 0xad, // Sequence number. + 2, 4, 6, 8, // RTP timestamp. + 0, 0, 1, 1, // SSRC. + 0b11000011, // Is key frame, has ref frame ID; has 3 extensions. + 64, // Frame ID. + 0x0, 0xb, // Packet ID. + 0x0, 0xc, // Max packet ID. + 64, // Reference Frame ID. + 8, 2, 0, 0, // Unknown extension with 2 bytes of data. + 4, 2, 1, 14, // Cast Adaptive Latency Extension data. + 16, 5, 0, 0, 0, 0, 0, // Unknown extension with 5 bytes of data. + 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29 // Payload. + }; + // clang-format on + const Ssrc kSenderSsrc = 0x00000101; + + RtpPacketParser parser(kSenderSsrc); + const auto result = parser.Parse(kInput); + ASSERT_TRUE(result); + EXPECT_EQ(RtpPayloadType::kAudioOpus, result->payload_type); + EXPECT_EQ(UINT16_C(0xdead), result->sequence_number); + EXPECT_EQ(RtpTimeTicks() + RtpTimeDelta::FromTicks(0x02040608), + result->rtp_timestamp); + EXPECT_TRUE(result->is_key_frame); + EXPECT_EQ(FrameId::first() + 64, result->frame_id); + EXPECT_EQ(FramePacketId{0x000b}, result->packet_id); + EXPECT_EQ(FramePacketId{0x000c}, result->max_packet_id); + EXPECT_EQ(FrameId::first() + 64, result->referenced_frame_id); + EXPECT_EQ(270, result->new_playout_delay.count()); + const absl::Span<const uint8_t> expected_payload(kInput + 34, 15); + ASSERT_EQ(expected_payload, result->payload); + EXPECT_TRUE(expected_payload == result->payload); +} + +// Tests that the parser ignores packets from an unknown source. +TEST(RtpPacketParserTest, IgnoresPacketWithWrongSsrc) { + // clang-format off + const uint8_t kInput[] = { + 0b10000000, // Version/Padding byte. + 96, // Payload type byte. + 0xbe, 0xef, // Sequence number. + 9, 8, 7, 6, // RTP timestamp. + 4, 3, 2, 1, // SSRC. + 0b10000000, // Is key frame, no extensions. + 5, // Frame ID. + 0xa, 0xb, // Packet ID. + 0xa, 0xc, // Max packet ID. + 0xf, 0xe, 0xd, 0xc, 0xb, 0xa, 0x9, 0x8, // Payload. + }; + // clang-format on + const Ssrc kSenderSsrc = 0x01020304; + + RtpPacketParser parser(kSenderSsrc); + const auto result = parser.Parse(kInput); + ASSERT_FALSE(result); +} + +// Tests that unexpected truncations in the RTP packets does not crash the +// parser, and that it correctly errors-out. +TEST(RtpPacketParserTest, RejectsTruncatedPackets) { + // clang-format off + const uint8_t kInput[] = { + 0b10000000, // Version/Padding byte. + 96, // Payload type byte. + 0xde, 0xad, // Sequence number. + 2, 4, 6, 8, // RTP timestamp. + 0, 0, 1, 1, // SSRC. + 0b11000011, // Is key frame, has ref frame ID; has 3 extensions. + 64, // Frame ID. + 0x0, 0xb, // Packet ID. + 0x0, 0xc, // Max packet ID. + 64, // Reference Frame ID. + 8, 2, 0, 0, // Unknown extension with 2 bytes of data. + 4, 2, 1, 14, // Cast Adaptive Latency Extension data. + 16, 5, 0, 0, 0, 0, 0, // Unknown extension with 5 bytes of data. + 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29 // Payload. + }; + // clang-format on + const Ssrc kSenderSsrc = 0x00000101; + + RtpPacketParser parser(kSenderSsrc); + ASSERT_FALSE(parser.Parse(absl::Span<const uint8_t>(kInput, 1))); + ASSERT_FALSE(parser.Parse(absl::Span<const uint8_t>(kInput, 18))); + ASSERT_FALSE(parser.Parse(absl::Span<const uint8_t>(kInput, 22))); + ASSERT_FALSE(parser.Parse(absl::Span<const uint8_t>(kInput, 33))); + + // When truncated to 34 bytes, the parser should see it as a packet with zero + // payload bytes. + const auto result_without_payload = + parser.Parse(absl::Span<const uint8_t>(kInput, 34)); + ASSERT_TRUE(result_without_payload); + EXPECT_TRUE(result_without_payload->payload.empty()); + + // And, of course, with the entire kInput available, the parser should see it + // as a packet with 15 bytes of payload. + const auto result_with_payload = + parser.Parse(absl::Span<const uint8_t>(kInput, sizeof(kInput))); + ASSERT_TRUE(result_with_payload); + EXPECT_EQ(size_t{15}, result_with_payload->payload.size()); +} + +// Tests that the parser rejects invalid packet ID values. +TEST(RtpPacketParserTest, RejectsPacketWithBadFramePacketIds) { + // clang-format off + const uint8_t kInput[] = { + 0b10000000, // Version/Padding byte. + 96, // Payload type byte. + 0xbe, 0xef, // Sequence number. + 9, 8, 7, 6, // RTP timestamp. + 1, 2, 3, 4, // SSRC. + 0b10000000, // Is key frame, no extensions. + 5, // Frame ID. + 0xa, 0xb, // Packet ID (which is GREATER than the max packet ID). + 0x0, 0x1, // Max packet ID. + 0xf, 0xe, 0xd, 0xc, 0xb, 0xa, 0x9, 0x8, // Payload. + }; + // clang-format on + const Ssrc kSenderSsrc = 0x01020304; + + // The parser should reject the packet because its packet ID field is greater + // than the max packet ID. + RtpPacketParser parser(kSenderSsrc); + ASSERT_FALSE(parser.Parse(kInput)); + + // Now, modify the packet such that its "max packet ID" field is set to the + // special "all packets lost" value. This makes the "packet ID" field valid, + // because it is less than the "max packet ID", but the "max packet ID" value + // itself is invalid. + uint8_t input_with_bad_max_packet_id[sizeof(kInput)]; + memcpy(input_with_bad_max_packet_id, kInput, sizeof(kInput)); + WriteBigEndian<uint16_t>(kAllPacketsLost, &input_with_bad_max_packet_id[16]); + const uint16_t packet_id = + ReadBigEndian<uint16_t>(&input_with_bad_max_packet_id[14]); + ASSERT_LE(packet_id, kAllPacketsLost); + ASSERT_FALSE(parser.Parse(input_with_bad_max_packet_id)); +} + +} // namespace +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/rtp_packetizer.cc b/cast/streaming/rtp_packetizer.cc new file mode 100644 index 00000000..675a4a97 --- /dev/null +++ b/cast/streaming/rtp_packetizer.cc @@ -0,0 +1,139 @@ +// 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/rtp_packetizer.h" + +#include <algorithm> +#include <limits> +#include <random> + +#include "cast/streaming/packet_util.h" +#include "platform/api/time.h" +#include "util/big_endian.h" +#include "util/integer_division.h" +#include "util/logging.h" + +using openscreen::platform::Clock; + +namespace cast { +namespace streaming { + +namespace { + +// Returns a random sequence number to start with. The reason for using a random +// number instead of zero is unclear, but this has existed both in several +// versions of the Cast Streaming spec and in other implementations for many +// years. +uint16_t GenerateRandomSequenceNumberStart() { + // Use a statically-allocated generator, instantiated upon first use, and + // seeded with the current time tick count. This generator was chosen because + // it is light-weight and does not need to produce unguessable (nor + // crypto-secure) values. + static std::minstd_rand generator(static_cast<std::minstd_rand::result_type>( + Clock::now().time_since_epoch().count())); + + return std::uniform_int_distribution<uint16_t>()(generator); +} + +} // namespace + +RtpPacketizer::RtpPacketizer(RtpPayloadType payload_type, + Ssrc sender_ssrc, + int max_packet_size) + : payload_type_7bits_(static_cast<uint8_t>(payload_type)), + sender_ssrc_(sender_ssrc), + max_packet_size_(max_packet_size), + sequence_number_(GenerateRandomSequenceNumberStart()) { + OSP_DCHECK(IsRtpPayloadType(payload_type_7bits_)); + OSP_DCHECK_GT(max_packet_size_, kMaxRtpHeaderSize); +} + +RtpPacketizer::~RtpPacketizer() = default; + +absl::Span<uint8_t> RtpPacketizer::GeneratePacket(const EncryptedFrame& frame, + FramePacketId packet_id, + absl::Span<uint8_t> buffer) { + OSP_CHECK_GE(static_cast<int>(buffer.size()), max_packet_size_); + + const int num_packets = ComputeNumberOfPackets(frame); + OSP_DCHECK_GT(num_packets, 0); + OSP_DCHECK_LT(int{packet_id}, num_packets); + const bool is_last_packet = int{packet_id} == (num_packets - 1); + + // Compute the size of this packet, which is the number of bytes of header + // plus the number of bytes of payload. Note that the optional Adaptive + // Latency information is only added to the first packet. + int packet_size = kBaseRtpHeaderSize; + const bool include_adaptive_latency_change = + (packet_id == 0 && + frame.new_playout_delay > std::chrono::milliseconds(0)); + if (include_adaptive_latency_change) { + OSP_DCHECK_LE(frame.new_playout_delay.count(), + int{std::numeric_limits<uint16_t>::max()}); + packet_size += kAdaptiveLatencyHeaderSize; + } + int data_chunk_size = max_payload_size(); + const int data_chunk_start = data_chunk_size * int{packet_id}; + if (is_last_packet) { + data_chunk_size = static_cast<int>(frame.data.size()) - data_chunk_start; + } + packet_size += data_chunk_size; + OSP_DCHECK_LE(packet_size, max_packet_size_); + const absl::Span<uint8_t> packet(buffer.data(), packet_size); + + // RTP Header. + AppendField<uint8_t>(kRtpRequiredFirstByte, &buffer); + AppendField<uint8_t>( + (is_last_packet ? kRtpMarkerBitMask : 0) | payload_type_7bits_, &buffer); + AppendField<uint16_t>(sequence_number_++, &buffer); + AppendField<uint32_t>(frame.rtp_timestamp.lower_32_bits(), &buffer); + AppendField<uint32_t>(sender_ssrc_, &buffer); + + // Cast Header. + AppendField<uint8_t>( + ((frame.dependency == EncodedFrame::KEY_FRAME) ? kRtpKeyFrameBitMask + : 0) | + kRtpHasReferenceFrameIdBitMask | + (include_adaptive_latency_change ? 1 : 0), + &buffer); + AppendField<uint8_t>(frame.frame_id.lower_8_bits(), &buffer); + AppendField<uint16_t>(packet_id, &buffer); + AppendField<uint16_t>(num_packets - 1, &buffer); + AppendField<uint8_t>(frame.referenced_frame_id.lower_8_bits(), &buffer); + + // Extension of Cast Header for Adaptive Latency change. + if (include_adaptive_latency_change) { + AppendField<uint16_t>( + (kAdaptiveLatencyRtpExtensionType << kNumExtensionDataSizeFieldBits) | + sizeof(uint16_t), + &buffer); + AppendField<uint16_t>(frame.new_playout_delay.count(), &buffer); + } + + // Sanity-check the pointer math, to ensure the packet is being entirely + // populated, with no underrun or overrun. + OSP_DCHECK_EQ(buffer.data() + data_chunk_size, packet.end()); + + // Copy the encrypted payload data into the packet. + memcpy(buffer.data(), frame.data.data() + data_chunk_start, data_chunk_size); + + return packet; +} + +int RtpPacketizer::ComputeNumberOfPackets(const EncryptedFrame& frame) const { + // The total number of packets is computed by assuming the payload will be + // split-up across as few packets as possible. + int num_packets = openscreen::DividePositivesRoundingUp( + static_cast<int>(frame.data.size()), max_payload_size()); + // Edge case: There must always be at least one packet, even when there are no + // payload bytes. Some audio codecs, for example, use zero bytes to represent + // a period of silence. + num_packets = std::max(1, num_packets); + + // Ensure that the entire range of FramePacketIds can be represented. + return num_packets <= int{kMaxAllowedFramePacketId} ? num_packets : -1; +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/rtp_packetizer.h b/cast/streaming/rtp_packetizer.h new file mode 100644 index 00000000..a3811948 --- /dev/null +++ b/cast/streaming/rtp_packetizer.h @@ -0,0 +1,81 @@ +// 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. + +#ifndef CAST_STREAMING_RTP_PACKETIZER_H_ +#define CAST_STREAMING_RTP_PACKETIZER_H_ + +#include <stdint.h> + +#include "absl/types/span.h" +#include "cast/streaming/frame_crypto.h" +#include "cast/streaming/rtp_defines.h" +#include "cast/streaming/ssrc.h" + +namespace cast { +namespace streaming { + +// Transforms a logical sequence of EncryptedFrames into RTP packets for +// transmission. A single instance of RtpPacketizer should be used for all the +// frames in a Cast RTP stream having the same SSRC. +class RtpPacketizer { + public: + // |payload_type| describes the type of the media content for the RTP stream + // from the sender having the given |sender_ssrc|. + // + // The |max_packet_size| argument depends on the optimal over-the-wire size of + // packets for the network medium being used. See discussion in rtp_defines.h + // for further info. + RtpPacketizer(RtpPayloadType payload_type, + Ssrc sender_ssrc, + int max_packet_size); + + ~RtpPacketizer(); + + // Wire-format one of the RTP packets for the given frame, which must only be + // transmitted once. This method should be called in the same sequence that + // packets will be transmitted. This also means that, if a packet needs to be + // re-transmitted, this method should be called to generate it again. Returns + // the subspan of |buffer| that contains the packet. |buffer| must be at least + // as large as the |max_packet_size| passed to the constructor. + absl::Span<uint8_t> GeneratePacket(const EncryptedFrame& frame, + FramePacketId packet_id, + absl::Span<uint8_t> buffer); + + // Given |frame|, compute the total number of packets over which the whole + // frame will be split-up. Returns -1 if the frame is too large and cannot be + // packetized. + int ComputeNumberOfPackets(const EncryptedFrame& frame) const; + + private: + int max_payload_size() const { + // Start with the configured max packet size, then subtract reserved space + // for packet header fields. The rest can be allocated to the payload. + return max_packet_size_ - kMaxRtpHeaderSize; + } + + // The validated ctor RtpPayloadType arg, in wire-format form. + const uint8_t payload_type_7bits_; + + const Ssrc sender_ssrc_; + const int max_packet_size_; + + // Incremented each time GeneratePacket() is called. Every packet, even those + // re-transmitted, must have different sequence numbers (within wrap-around + // concerns) per the RTP spec. + uint16_t sequence_number_; + + // See rtp_defines.h for wire-format diagram. + static constexpr int kBaseRtpHeaderSize = + // Plus one byte, because this implementation always includes the 8-bit + // Reference Frame ID field. + kRtpPacketMinValidSize + 1; + static constexpr int kAdaptiveLatencyHeaderSize = 4; + static constexpr int kMaxRtpHeaderSize = + kBaseRtpHeaderSize + kAdaptiveLatencyHeaderSize; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_RTP_PACKETIZER_H_ diff --git a/cast/streaming/rtp_packetizer_unittest.cc b/cast/streaming/rtp_packetizer_unittest.cc new file mode 100644 index 00000000..52b460de --- /dev/null +++ b/cast/streaming/rtp_packetizer_unittest.cc @@ -0,0 +1,207 @@ +// 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/rtp_packetizer.h" + +#include "absl/types/optional.h" +#include "cast/streaming/frame_crypto.h" +#include "cast/streaming/rtp_defines.h" +#include "cast/streaming/rtp_packet_parser.h" +#include "cast/streaming/ssrc.h" +#include "gtest/gtest.h" + +namespace cast { +namespace streaming { +namespace { + +constexpr RtpPayloadType kPayloadType = RtpPayloadType::kAudioOpus; + +// Returns true if |needle| is fully within |haystack|. +bool IsSubspan(absl::Span<const uint8_t> needle, + absl::Span<const uint8_t> haystack) { + return (needle.data() >= haystack.data()) && + ((needle.data() + needle.size()) <= + (haystack.data() + haystack.size())); +} + +class RtpPacketizerTest : public testing::Test { + public: + RtpPacketizerTest() = default; + ~RtpPacketizerTest() = default; + + RtpPacketizer* packetizer() { return &packetizer_; } + + EncryptedFrame CreateFrame(FrameId frame_id, + bool is_key_frame, + std::chrono::milliseconds new_playout_delay, + int payload_size) const { + EncodedFrame frame; + frame.dependency = is_key_frame ? EncodedFrame::KEY_FRAME + : EncodedFrame::DEPENDS_ON_ANOTHER; + frame.frame_id = frame_id; + frame.referenced_frame_id = is_key_frame ? frame_id : (frame_id - 1); + frame.rtp_timestamp = RtpTimeTicks() + RtpTimeDelta::FromTicks(987); + frame.reference_time = openscreen::platform::Clock::now(); + frame.new_playout_delay = new_playout_delay; + + std::unique_ptr<uint8_t[]> buffer(new uint8_t[payload_size]); + for (int i = 0; i < payload_size; ++i) { + buffer[i] = static_cast<uint8_t>(i); + } + frame.data = absl::Span<uint8_t>(buffer.get(), payload_size); + + return crypto_.Encrypt(frame); + } + + // Generates one of the frame's packets, then parses it and checks for the + // expected values. Thus, this test assumes PacketParser is already working + // (i.e., all RtpPacketParser unit tests are passing). + void TestGeneratePacket(const EncryptedFrame& frame, + FramePacketId packet_id) { + SCOPED_TRACE(testing::Message() << "packet_id=" << packet_id); + + const int frame_payload_size = frame.data.size(); + constexpr int kExpectedRtpHeaderSize = 23; + const int packet_payload_size = + kMaxRtpPacketSizeForIpv4UdpOnEthernet - kExpectedRtpHeaderSize; + const int final_packet_payload_size = + frame_payload_size % packet_payload_size; + const int num_packets = 1 + frame_payload_size / packet_payload_size; + + // Generate a RTP packet and parse it. + uint8_t scratch[kMaxRtpPacketSizeForIpv4UdpOnEthernet]; + memset(scratch, 0, sizeof(scratch)); + const auto packet = packetizer_.GeneratePacket(frame, packet_id, scratch); + ASSERT_TRUE(IsSubspan(packet, scratch)); + + const auto result = parser_.Parse(packet); + ASSERT_TRUE(result); + + // Check that RTP header fields match expected values. + EXPECT_EQ(kPayloadType, result->payload_type); + EXPECT_EQ(frame.rtp_timestamp, result->rtp_timestamp); + EXPECT_EQ(frame.dependency == EncodedFrame::KEY_FRAME, + result->is_key_frame); + EXPECT_EQ(frame.frame_id, result->frame_id); + EXPECT_EQ(packet_id, result->packet_id); + EXPECT_EQ(static_cast<FramePacketId>(num_packets - 1), + result->max_packet_id); + EXPECT_EQ(frame.referenced_frame_id, result->referenced_frame_id); + + // The sequence number field MUST be different for each packet, regardless + // of whether the exact same packet is being re-generated. + if (last_sequence_number_) { + EXPECT_EQ(static_cast<uint16_t>(*last_sequence_number_ + 1), + result->sequence_number); + } + last_sequence_number_ = result->sequence_number; + + // If there is a playout delay change starting with this |frame|, it must + // only be mentioned in the first packet. + if (packet_id == FramePacketId{0}) { + EXPECT_EQ(frame.new_playout_delay, result->new_playout_delay); + } else { + EXPECT_EQ(std::chrono::milliseconds(0), result->new_playout_delay); + } + + // Check that the RTP payload is correct for this packet. + ASSERT_TRUE(IsSubspan(result->payload, packet)); + // Last packet is smaller, as its payload is just the remaining bytes. + const int expected_payload_size = (int{packet_id} == (num_packets - 1)) + ? final_packet_payload_size + : packet_payload_size; + EXPECT_EQ(expected_payload_size, static_cast<int>(result->payload.size())); + const absl::Span<const uint8_t> expected_bytes( + frame.data.data() + (packet_id * packet_payload_size), + expected_payload_size); + EXPECT_EQ(expected_bytes, result->payload); + } + + private: + // The RtpPacketizer instance under test, plus some surrounding dependencies + // to generate its input and examine its output. + const Ssrc ssrc_{GenerateSsrc(true)}; + const FrameCrypto crypto_{FrameCrypto::GenerateRandomBytes(), + FrameCrypto::GenerateRandomBytes()}; + RtpPacketizer packetizer_{kPayloadType, ssrc_, + kMaxRtpPacketSizeForIpv4UdpOnEthernet}; + RtpPacketParser parser_{ssrc_}; + + // absl::nullopt until the random starting sequence number, from the first + // packet generated by TestGeneratePacket(), is known. + absl::optional<uint16_t> last_sequence_number_; +}; + +// Tests that all packets are generated for one key frame, followed by 9 "delta" +// frames. The key frame is larger than the other frames, as is typical in a +// real-world usage scenario. +TEST_F(RtpPacketizerTest, GeneratesPacketsForSequenceOfFrames) { + for (int i = 0; i < 10; ++i) { + const bool is_key_frame = (i == 0); + const int frame_payload_size = is_key_frame ? 48269 : 10000; + const EncryptedFrame frame = + CreateFrame(FrameId::first() + i, is_key_frame, + std::chrono::milliseconds(0), frame_payload_size); + SCOPED_TRACE(testing::Message() << "frame_id=" << frame.frame_id); + const int num_packets = packetizer()->ComputeNumberOfPackets(frame); + ASSERT_EQ(is_key_frame ? 34 : 7, num_packets); + + for (int j = 0; j < num_packets; ++j) { + TestGeneratePacket(frame, static_cast<FramePacketId>(j)); + if (testing::Test::HasFailure()) { + return; + } + } + } +} + +// Tests that all packets are generated for a key frame that includes a playout +// delay change. Only the first packet should mention the playout delay change. +TEST_F(RtpPacketizerTest, GeneratesPacketsForFrameWithLatencyChange) { + const int frame_payload_size = 38383; + const EncryptedFrame frame = + CreateFrame(FrameId::first() + 42, true, std::chrono::milliseconds(543), + frame_payload_size); + const int num_packets = packetizer()->ComputeNumberOfPackets(frame); + ASSERT_EQ(27, num_packets); + + for (int i = 0; i < num_packets; ++i) { + TestGeneratePacket(frame, static_cast<FramePacketId>(i)); + if (testing::Test::HasFailure()) { + return; + } + } +} + +// Tests that a single, valid RTP packet is generated for a frame with no data +// payload. Having no payload is valid with some codecs (e.g., complete audio +// silence can be represented by an empty payload). +TEST_F(RtpPacketizerTest, GeneratesOnePacketForFrameWithNoPayload) { + const int frame_payload_size = 0; + const EncryptedFrame frame = + CreateFrame(FrameId::first() + 99, false, std::chrono::milliseconds(0), + frame_payload_size); + ASSERT_EQ(1, packetizer()->ComputeNumberOfPackets(frame)); + TestGeneratePacket(frame, FramePacketId{0}); +} + +// Tests that re-generating the same packet for re-transmission works, including +// a different sequence counter value in the packet each time. +TEST_F(RtpPacketizerTest, GeneratesPacketForRetransmission) { + const int frame_payload_size = 16384; + const EncryptedFrame frame = CreateFrame( + FrameId::first(), true, std::chrono::milliseconds(0), frame_payload_size); + const int num_packets = packetizer()->ComputeNumberOfPackets(frame); + ASSERT_EQ(12, num_packets); + + for (int i = 0; i < 10; ++i) { + // Keep generating the same packet. TestGeneratePacket() will check that a + // different sequence number is used each time. + TestGeneratePacket(frame, FramePacketId{3}); + } +} + +} // namespace +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/rtp_time.cc b/cast/streaming/rtp_time.cc new file mode 100644 index 00000000..3f73715e --- /dev/null +++ b/cast/streaming/rtp_time.cc @@ -0,0 +1,25 @@ +// Copyright 2015 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/rtp_time.h" + +#include <sstream> + +namespace cast { +namespace streaming { + +std::ostream& operator<<(std::ostream& out, const RtpTimeDelta rhs) { + if (rhs.value_ >= 0) + out << "RTP+"; + else + out << "RTP"; + return out << rhs.value_; +} + +std::ostream& operator<<(std::ostream& out, const RtpTimeTicks rhs) { + return out << "RTP@" << rhs.value_; +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/rtp_time.h b/cast/streaming/rtp_time.h new file mode 100644 index 00000000..0a7f5d86 --- /dev/null +++ b/cast/streaming/rtp_time.h @@ -0,0 +1,268 @@ +// Copyright 2015 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. + +#ifndef CAST_STREAMING_RTP_TIME_H_ +#define CAST_STREAMING_RTP_TIME_H_ + +#include <stdint.h> + +#include <chrono> +#include <cmath> +#include <limits> +#include <sstream> + +#include "cast/streaming/expanded_value_base.h" +#include "platform/api/time.h" + +namespace cast { +namespace streaming { + +// Forward declarations (see below). +class RtpTimeDelta; +class RtpTimeTicks; + +// Convenience operator overloads for logging. +std::ostream& operator<<(std::ostream& out, const RtpTimeDelta rhs); +std::ostream& operator<<(std::ostream& out, const RtpTimeTicks rhs); + +// The difference between two RtpTimeTicks values. This data type is modeled +// off of Chromium's base::TimeDelta, and used for performing compiler-checked +// arithmetic with RtpTimeTicks. +// +// This data type wraps a value, providing only the meaningful set of math +// operations that may be performed on the value. RtpTimeDeltas may be +// added/subtracted with other RtpTimeDeltas to produce a RtpTimeDelta holding +// the sum/difference. RtpTimeDeltas may also be multiplied or divided by +// integer amounts. Finally, RtpTimeDeltas may be divided by other +// RtpTimeDeltas to compute a number of periods (trunc'ed to an integer), or +// modulo each other to determine a time period remainder. +// +// The base class provides bit truncation/extension features for +// wire-formatting, and also the comparison operators. +// +// Usage example: +// +// // Time math. +// RtpTimeDelta zero; +// RtpTimeDelta one_second_later = +// zero + RtpTimeDelta::FromTicks(kAudioSamplingRate); +// RtpTimeDelta ten_seconds_later = one_second_later * 10; +// int64_t ten_periods = ten_seconds_later / one_second_later; +// +// // Logging convenience. +// OSP_DLOG_INFO << "The RTP time offset is " << ten_seconds_later; +// +// // Convert (approximately!) between RTP timebase and microsecond timebase: +// RtpTimeDelta nine_seconds_in_rtp = ten_seconds_later - one_second_later; +// using std::chrono::microseconds; +// microseconds nine_seconds_duration = +// nine_seconds_in_rtp.ToDuration<microseconds>(kAudioSamplingRate); +// RtpTimeDelta two_seconds_in_rtp = +// RtpTimeDelta::FromDuration(std::chrono::seconds(2), +// kAudioSamplingRate); +class RtpTimeDelta : public ExpandedValueBase<int64_t, RtpTimeDelta> { + public: + constexpr RtpTimeDelta() : ExpandedValueBase(0) {} + + // Arithmetic operators (with other deltas). + constexpr RtpTimeDelta operator+(RtpTimeDelta rhs) const { + return RtpTimeDelta(value_ + rhs.value_); + } + constexpr RtpTimeDelta operator-(RtpTimeDelta rhs) const { + return RtpTimeDelta(value_ - rhs.value_); + } + constexpr RtpTimeDelta& operator+=(RtpTimeDelta rhs) { + return (*this = (*this + rhs)); + } + constexpr RtpTimeDelta& operator-=(RtpTimeDelta rhs) { + return (*this = (*this - rhs)); + } + constexpr RtpTimeDelta operator-() const { return RtpTimeDelta(-value_); } + + // Multiplicative operators (with other deltas). + constexpr int64_t operator/(RtpTimeDelta rhs) const { + return value_ / rhs.value_; + } + constexpr RtpTimeDelta operator%(RtpTimeDelta rhs) const { + return RtpTimeDelta(value_ % rhs.value_); + } + constexpr RtpTimeDelta& operator%=(RtpTimeDelta rhs) { + return (*this = (*this % rhs)); + } + + // Multiplicative operators (with integer types). + template <typename IntType> + constexpr RtpTimeDelta operator*(IntType rhs) const { + static_assert(std::numeric_limits<IntType>::is_integer, + "|rhs| must be a POD integer type"); + return RtpTimeDelta(value_ * rhs); + } + template <typename IntType> + constexpr RtpTimeDelta operator/(IntType rhs) const { + static_assert(std::numeric_limits<IntType>::is_integer, + "|rhs| must be a POD integer type"); + return RtpTimeDelta(value_ / rhs); + } + template <typename IntType> + constexpr RtpTimeDelta& operator*=(IntType rhs) { + return (*this = (*this * rhs)); + } + template <typename IntType> + constexpr RtpTimeDelta& operator/=(IntType rhs) { + return (*this = (*this / rhs)); + } + + // Maps this RtpTimeDelta to an approximate std::chrono::duration using the + // given RTP timebase. Assumes a zero-valued Duration corresponds to a + // zero-valued RtpTimeDelta. + template <typename Duration> + Duration ToDuration(int rtp_timebase) const { + OSP_DCHECK_GT(rtp_timebase, 0); + constexpr Duration kOneSecond = + std::chrono::duration_cast<Duration>(std::chrono::seconds(1)); + return Duration(ToNearestRepresentativeValue<typename Duration::rep>( + static_cast<double>(value_) / rtp_timebase * kOneSecond.count())); + } + + // Maps the |duration| to an approximate RtpTimeDelta using the given RTP + // timebase. Assumes a zero-valued Duration corresponds to a zero-valued + // RtpTimeDelta. + template <typename Duration> + static constexpr RtpTimeDelta FromDuration(Duration duration, + int rtp_timebase) { + constexpr Duration kOneSecond = + std::chrono::duration_cast<Duration>(std::chrono::seconds(1)); + static_assert(kOneSecond > Duration::zero(), + "Duration is too coarse-grained to represent one second."); + return RtpTimeDelta(ToNearestRepresentativeValue<int64_t>( + static_cast<double>(duration.count()) / kOneSecond.count() * + rtp_timebase)); + } + + // Construct a RtpTimeDelta from an exact number of ticks. + static constexpr RtpTimeDelta FromTicks(int64_t ticks) { + return RtpTimeDelta(ticks); + } + + private: + friend class ExpandedValueBase<int64_t, RtpTimeDelta>; + friend class RtpTimeTicks; + friend std::ostream& operator<<(std::ostream& out, const RtpTimeDelta rhs); + + constexpr explicit RtpTimeDelta(int64_t ticks) : ExpandedValueBase(ticks) {} + + constexpr int64_t value() const { return value_; } + + template <typename Rep> + static Rep ToNearestRepresentativeValue(double ticks) { + if (ticks <= std::numeric_limits<Rep>::min()) { + return std::numeric_limits<Rep>::min(); + } else if (ticks >= std::numeric_limits<Rep>::max()) { + return std::numeric_limits<Rep>::max(); + } + + static_assert( + std::is_floating_point<Rep>::value || + (std::is_integral<Rep>::value && + sizeof(Rep) <= sizeof(decltype(llround(ticks)))), + "Rep must be an integer (<= 64 bits) or a floating-point type."); + if (std::is_floating_point<Rep>::value) { + return Rep(ticks); + } + if (sizeof(Rep) <= sizeof(decltype(lround(ticks)))) { + return Rep(lround(ticks)); + } + return Rep(llround(ticks)); + } +}; + +// A media timestamp whose timebase matches the periodicity of the content +// (e.g., for audio, the timebase would be the sampling frequency). This data +// type is modeled off of Chromium's base::TimeTicks. +// +// This data type wraps a value, providing only the meaningful set of math +// operations that may be performed on the value. The difference between two +// RtpTimeTicks is a RtpTimeDelta. Likewise, adding or subtracting a +// RtpTimeTicks with a RtpTimeDelta produces an off-set RtpTimeTicks. +// +// The base class provides bit truncation/extension features for +// wire-formatting, and also the comparison operators. +// +// Usage example: +// +// // Time math. +// RtpTimeTicks origin; +// RtpTimeTicks at_one_second = +// origin + RtpTimeDelta::FromTicks(kAudioSamplingRate); +// RtpTimeTicks at_two_seconds = +// at_one_second + RtpTimeDelta::FromTicks(kAudioSamplingRate); +// RtpTimeDelta elasped_in_between = at_two_seconds - at_one_second; +// RtpTimeDelta thrice_as_much_elasped = elasped_in_between * 3; +// RtpTimeTicks at_four_seconds = at_one_second + thrice_as_much_elasped; +// +// // Logging convenience. +// OSP_DLOG_INFO << "The RTP timestamp is " << at_four_seconds; +// +// // Convert (approximately!) between RTP timebase and stream time offsets in +// // microsecond timebase: +// using std::chrono::microseconds; +// microseconds four_seconds_since_stream_start = +// at_four_seconds.ToTimeSinceOrigin<microseconds>(kAudioSamplingRate); +// RtpTimeTicks at_three_seconds = RtpTimeDelta::FromTimeSinceOrigin( +// std::chrono::seconds(3), kAudioSamplingRate); +class RtpTimeTicks : public ExpandedValueBase<int64_t, RtpTimeTicks> { + public: + constexpr RtpTimeTicks() : ExpandedValueBase(0) {} + + // Compute the difference between two RtpTimeTickses. + constexpr RtpTimeDelta operator-(RtpTimeTicks rhs) const { + return RtpTimeDelta(value_ - rhs.value_); + } + + // Return a new RtpTimeTicks before or after this one. + constexpr RtpTimeTicks operator+(RtpTimeDelta rhs) const { + return RtpTimeTicks(value_ + rhs.value()); + } + constexpr RtpTimeTicks operator-(RtpTimeDelta rhs) const { + return RtpTimeTicks(value_ - rhs.value()); + } + constexpr RtpTimeTicks& operator+=(RtpTimeDelta rhs) { + return (*this = (*this + rhs)); + } + constexpr RtpTimeTicks& operator-=(RtpTimeDelta rhs) { + return (*this = (*this - rhs)); + } + + // Maps this RtpTimeTicks to an approximate std::chrono::duration representing + // the amount of time since the origin point (e.g., the start of a stream) + // using the given |rtp_timebase|. Assumes a zero-valued Duration corresponds + // to a zero-valued RtpTimeTicks. + template <typename Duration> + Duration ToTimeSinceOrigin(int rtp_timebase) const { + return (*this - RtpTimeTicks()).ToDuration<Duration>(rtp_timebase); + } + + // Maps the |time_since_origin| to an approximate RtpTimeTicks using the given + // RTP timebase. Assumes a zero-valued Duration corresponds to a zero-valued + // RtpTimeTicks. + template <typename Duration> + static constexpr RtpTimeTicks FromTimeSinceOrigin(Duration time_since_origin, + int rtp_timebase) { + return RtpTimeTicks() + + RtpTimeDelta::FromDuration(time_since_origin, rtp_timebase); + } + + private: + friend class ExpandedValueBase<int64_t, RtpTimeTicks>; + friend std::ostream& operator<<(std::ostream& out, const RtpTimeTicks rhs); + + constexpr explicit RtpTimeTicks(int64_t value) : ExpandedValueBase(value) {} + + constexpr int64_t value() const { return value_; } +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_RTP_TIME_H_ diff --git a/cast/streaming/rtp_time_unittest.cc b/cast/streaming/rtp_time_unittest.cc new file mode 100644 index 00000000..44312e7b --- /dev/null +++ b/cast/streaming/rtp_time_unittest.cc @@ -0,0 +1,75 @@ +// Copyright 2015 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/rtp_time.h" + +#include "gtest/gtest.h" + +namespace cast { +namespace streaming { + +// Tests that conversions between std::chrono durations and RtpTimeDelta are +// accurate. Note that this implicitly tests the conversions to/from +// RtpTimeTicks as well due to shared implementation. +TEST(RtpTimeDeltaTest, ConversionToAndFromDurations) { + using std::chrono::microseconds; + using std::chrono::milliseconds; + using std::chrono::seconds; + + constexpr int kTimebase = 48000; + + // Origin in both timelines is equivalent. + ASSERT_EQ(RtpTimeDelta(), RtpTimeDelta::FromTicks(0)); + ASSERT_EQ(RtpTimeDelta(), + RtpTimeDelta::FromDuration(microseconds(0), kTimebase)); + ASSERT_EQ(microseconds::zero(), + RtpTimeDelta::FromTicks(0).ToDuration<microseconds>(kTimebase)); + + // Conversions that are exact (i.e., do not require rounding). + ASSERT_EQ(RtpTimeDelta::FromTicks(480), + RtpTimeDelta::FromDuration(milliseconds(10), kTimebase)); + ASSERT_EQ(RtpTimeDelta::FromTicks(96000), + RtpTimeDelta::FromDuration(seconds(2), kTimebase)); + ASSERT_EQ(milliseconds(10), + RtpTimeDelta::FromTicks(480).ToDuration<microseconds>(kTimebase)); + ASSERT_EQ(seconds(2), + RtpTimeDelta::FromTicks(96000).ToDuration<microseconds>(kTimebase)); + + // Conversions that are approximate (i.e., are rounded). + for (int error_us = -3; error_us <= +3; ++error_us) { + ASSERT_EQ( + RtpTimeDelta::FromTicks(0), + RtpTimeDelta::FromDuration(microseconds(0 + error_us), kTimebase)); + ASSERT_EQ( + RtpTimeDelta::FromTicks(1), + RtpTimeDelta::FromDuration(microseconds(21 + error_us), kTimebase)); + ASSERT_EQ( + RtpTimeDelta::FromTicks(2), + RtpTimeDelta::FromDuration(microseconds(42 + error_us), kTimebase)); + ASSERT_EQ( + RtpTimeDelta::FromTicks(3), + RtpTimeDelta::FromDuration(microseconds(63 + error_us), kTimebase)); + ASSERT_EQ( + RtpTimeDelta::FromTicks(4), + RtpTimeDelta::FromDuration(microseconds(83 + error_us), kTimebase)); + ASSERT_EQ( + RtpTimeDelta::FromTicks(11200000000000), + RtpTimeDelta::FromDuration( + microseconds(INT64_C(233333333333333) + error_us), kTimebase)); + } + ASSERT_EQ(microseconds(21), + RtpTimeDelta::FromTicks(1).ToDuration<microseconds>(kTimebase)); + ASSERT_EQ(microseconds(42), + RtpTimeDelta::FromTicks(2).ToDuration<microseconds>(kTimebase)); + ASSERT_EQ(microseconds(63), + RtpTimeDelta::FromTicks(3).ToDuration<microseconds>(kTimebase)); + ASSERT_EQ(microseconds(83), + RtpTimeDelta::FromTicks(4).ToDuration<microseconds>(kTimebase)); + ASSERT_EQ(microseconds(INT64_C(233333333333333)), + RtpTimeDelta::FromTicks(11200000000000) + .ToDuration<microseconds>(kTimebase)); +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/sender_report_builder.cc b/cast/streaming/sender_report_builder.cc new file mode 100644 index 00000000..56d100d8 --- /dev/null +++ b/cast/streaming/sender_report_builder.cc @@ -0,0 +1,56 @@ +// 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/sender_report_builder.h" + +#include "cast/streaming/packet_util.h" +#include "util/logging.h" + +namespace cast { +namespace streaming { + +SenderReportBuilder::SenderReportBuilder(RtcpSession* session) + : session_(session) { + OSP_DCHECK(session_); +} + +SenderReportBuilder::~SenderReportBuilder() = default; + +std::pair<absl::Span<uint8_t>, StatusReportId> SenderReportBuilder::BuildPacket( + const RtcpSenderReport& sender_report, + absl::Span<uint8_t> buffer) const { + OSP_CHECK_GE(buffer.size(), kRequiredBufferSize); + + uint8_t* const packet_begin = buffer.data(); + + RtcpCommonHeader header; + header.packet_type = RtcpPacketType::kSenderReport; + header.payload_size = kRtcpSenderReportSize; + if (sender_report.report_block) { + header.with.report_count = 1; + header.payload_size += kRtcpReportBlockSize; + } else { + header.with.report_count = 0; + } + header.AppendFields(&buffer); + + AppendField<uint32_t>(session_->sender_ssrc(), &buffer); + const NtpTimestamp ntp_timestamp = + session_->ntp_converter().ToNtpTimestamp(sender_report.reference_time); + AppendField<uint64_t>(ntp_timestamp, &buffer); + AppendField<uint32_t>(sender_report.rtp_timestamp.lower_32_bits(), &buffer); + AppendField<uint32_t>(sender_report.send_packet_count, &buffer); + AppendField<uint32_t>(sender_report.send_octet_count, &buffer); + if (sender_report.report_block) { + sender_report.report_block->AppendFields(&buffer); + } + + uint8_t* const packet_end = buffer.data(); + return std::make_pair( + absl::Span<uint8_t>(packet_begin, packet_end - packet_begin), + ToStatusReportId(ntp_timestamp)); +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/sender_report_builder.h b/cast/streaming/sender_report_builder.h new file mode 100644 index 00000000..724e7342 --- /dev/null +++ b/cast/streaming/sender_report_builder.h @@ -0,0 +1,45 @@ +// 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. + +#ifndef CAST_STREAMING_SENDER_REPORT_BUILDER_H_ +#define CAST_STREAMING_SENDER_REPORT_BUILDER_H_ + +#include <stdint.h> + +#include <utility> + +#include "absl/types/span.h" +#include "cast/streaming/rtcp_common.h" +#include "cast/streaming/rtcp_session.h" +#include "cast/streaming/rtp_defines.h" + +namespace cast { +namespace streaming { + +// Builds RTCP packets containing one Sender Report. +class SenderReportBuilder { + public: + explicit SenderReportBuilder(RtcpSession* session); + ~SenderReportBuilder(); + + // Serializes the given |sender_report| as a RTCP packet and writes it to + // |buffer| (which must be kRequiredBufferSize in size). Returns the subspan + // of |buffer| that contains the result and a StatusReportId the receiver + // might use in its own reports to reference this specific report. + std::pair<absl::Span<uint8_t>, StatusReportId> BuildPacket( + const RtcpSenderReport& sender_report, + absl::Span<uint8_t> buffer) const; + + // The required size (in bytes) of the buffer passed to BuildPacket(). + static constexpr int kRequiredBufferSize = + kRtcpCommonHeaderSize + kRtcpSenderReportSize + kRtcpReportBlockSize; + + private: + RtcpSession* const session_; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_SENDER_REPORT_BUILDER_H_ diff --git a/cast/streaming/sender_report_parser.cc b/cast/streaming/sender_report_parser.cc new file mode 100644 index 00000000..4770dd6b --- /dev/null +++ b/cast/streaming/sender_report_parser.cc @@ -0,0 +1,75 @@ +// 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/sender_report_parser.h" + +#include "cast/streaming/packet_util.h" +#include "util/logging.h" + +namespace cast { +namespace streaming { + +SenderReportParser::SenderReportWithId::SenderReportWithId() = default; +SenderReportParser::SenderReportWithId::~SenderReportWithId() = default; + +SenderReportParser::SenderReportParser(RtcpSession* session) + : session_(session) { + OSP_DCHECK(session_); +} + +SenderReportParser::~SenderReportParser() = default; + +absl::optional<SenderReportParser::SenderReportWithId> +SenderReportParser::Parse(absl::Span<const uint8_t> buffer) { + absl::optional<SenderReportWithId> sender_report; + + // 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 absl::nullopt; + } + buffer.remove_prefix(kRtcpCommonHeaderSize); + if (static_cast<int>(buffer.size()) < header->payload_size) { + return absl::nullopt; + } + auto chunk = buffer.subspan(0, header->payload_size); + buffer.remove_prefix(header->payload_size); + + // Only process Sender Reports with a matching SSRC. + if (header->packet_type != RtcpPacketType::kSenderReport) { + continue; + } + if (header->payload_size < kRtcpSenderReportSize) { + return absl::nullopt; + } + if (ConsumeField<uint32_t>(&chunk) != session_->sender_ssrc()) { + continue; + } + SenderReportWithId& report = sender_report.emplace(); + const NtpTimestamp ntp_timestamp = ConsumeField<uint64_t>(&chunk); + report.report_id = ToStatusReportId(ntp_timestamp); + report.reference_time = + session_->ntp_converter().ToLocalTime(ntp_timestamp); + report.rtp_timestamp = + last_parsed_rtp_timestamp_.Expand(ConsumeField<uint32_t>(&chunk)); + report.send_packet_count = ConsumeField<uint32_t>(&chunk); + report.send_octet_count = ConsumeField<uint32_t>(&chunk); + report.report_block = RtcpReportBlock::ParseOne( + chunk, header->with.report_count, session_->receiver_ssrc()); + } + + // At this point, the packet is known to be well-formed. Cache the + // most-recently parsed RTP timestamp value for bit-expansion in future + // parses. + if (sender_report) { + last_parsed_rtp_timestamp_ = sender_report->rtp_timestamp; + } + return sender_report; +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/sender_report_parser.h b/cast/streaming/sender_report_parser.h new file mode 100644 index 00000000..df7f89a0 --- /dev/null +++ b/cast/streaming/sender_report_parser.h @@ -0,0 +1,51 @@ +// 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. + +#ifndef CAST_STREAMING_SENDER_REPORT_PARSER_H_ +#define CAST_STREAMING_SENDER_REPORT_PARSER_H_ + +#include "absl/types/optional.h" +#include "absl/types/span.h" +#include "cast/streaming/rtcp_common.h" +#include "cast/streaming/rtcp_session.h" +#include "cast/streaming/rtp_defines.h" +#include "cast/streaming/rtp_time.h" + +namespace cast { +namespace streaming { + +// Parses RTCP packets from a Sender to extract Sender Reports. Ignores anything +// else, since that is all a Receiver would be interested in. +class SenderReportParser { + public: + // Returned by Parse(), to also separately expose the StatusReportId. The + // report ID isn't included in the common RtcpSenderReport struct because it's + // not an input to SenderReportBuilder (it is generated by the builder). + struct SenderReportWithId : public RtcpSenderReport { + SenderReportWithId(); + ~SenderReportWithId(); + + StatusReportId report_id{}; + }; + + explicit SenderReportParser(RtcpSession* session); + ~SenderReportParser(); + + // Parses the RTCP |packet|, and returns a parsed sender report if the packet + // contained one. Returns nullopt if the data is corrupt or the packet did not + // contain a sender report. + absl::optional<SenderReportWithId> Parse(absl::Span<const uint8_t> packet); + + private: + RtcpSession* const session_; + + // Tracks the recently-parsed RTP timestamps so that the truncated values can + // be re-expanded into full-form. + RtpTimeTicks last_parsed_rtp_timestamp_; +}; + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_SENDER_REPORT_PARSER_H_ diff --git a/cast/streaming/sender_report_parser_fuzzer.cc b/cast/streaming/sender_report_parser_fuzzer.cc new file mode 100644 index 00000000..8b9d8e74 --- /dev/null +++ b/cast/streaming/sender_report_parser_fuzzer.cc @@ -0,0 +1,48 @@ +// 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 <stdint.h> + +#include "cast/streaming/sender_report_parser.h" +#include "platform/api/time.h" + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + using cast::streaming::RtcpSenderReport; + using cast::streaming::RtcpSession; + using cast::streaming::SenderReportParser; + using cast::streaming::Ssrc; + + constexpr Ssrc kSenderSsrcInSeedCorpus = 1; + constexpr Ssrc kReceiverSsrcInSeedCorpus = 2; + + // Allocate the RtcpSession and SenderReportParser statically (i.e., one-time + // init) to improve the fuzzer's execution rate. This is because RtcpSession + // also contains a NtpTimeConverter, which samples the system clock at + // construction time. There is no reason to re-construct these objects for + // each fuzzer test input. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wexit-time-destructors" + static RtcpSession session(kSenderSsrcInSeedCorpus, kReceiverSsrcInSeedCorpus, + openscreen::platform::Clock::now()); + static SenderReportParser parser(&session); +#pragma clang diagnostic pop + + parser.Parse(absl::Span<const uint8_t>(data, size)); + + return 0; +} + +#if defined(NEEDS_MAIN_TO_CALL_FUZZER_DRIVER) + +// Forward declarations of Clang's built-in libFuzzer driver. +namespace fuzzer { +using TestOneInputCallback = int (*)(const uint8_t* data, size_t size); +int FuzzerDriver(int* argc, char*** argv, TestOneInputCallback callback); +} // namespace fuzzer + +int main(int argc, char* argv[]) { + return fuzzer::FuzzerDriver(&argc, &argv, LLVMFuzzerTestOneInput); +} + +#endif // defined(NEEDS_MAIN_TO_CALL_FUZZER_DRIVER) diff --git a/cast/streaming/sender_report_parser_fuzzer_seeds/compound_packet_with_other_then_sr.bin b/cast/streaming/sender_report_parser_fuzzer_seeds/compound_packet_with_other_then_sr.bin Binary files differnew file mode 100644 index 00000000..9ff47886 --- /dev/null +++ b/cast/streaming/sender_report_parser_fuzzer_seeds/compound_packet_with_other_then_sr.bin diff --git a/cast/streaming/sender_report_parser_fuzzer_seeds/compound_packet_with_sr_then_other.bin b/cast/streaming/sender_report_parser_fuzzer_seeds/compound_packet_with_sr_then_other.bin Binary files differnew file mode 100644 index 00000000..646db5aa --- /dev/null +++ b/cast/streaming/sender_report_parser_fuzzer_seeds/compound_packet_with_sr_then_other.bin diff --git a/cast/streaming/sender_report_parser_fuzzer_seeds/rtcp_packet_with_no_sender_report.bin b/cast/streaming/sender_report_parser_fuzzer_seeds/rtcp_packet_with_no_sender_report.bin Binary files differnew file mode 100644 index 00000000..a5c35e0a --- /dev/null +++ b/cast/streaming/sender_report_parser_fuzzer_seeds/rtcp_packet_with_no_sender_report.bin diff --git a/cast/streaming/sender_report_parser_fuzzer_seeds/sender_report_with_report_block.bin b/cast/streaming/sender_report_parser_fuzzer_seeds/sender_report_with_report_block.bin Binary files differnew file mode 100644 index 00000000..eb4b0d80 --- /dev/null +++ b/cast/streaming/sender_report_parser_fuzzer_seeds/sender_report_with_report_block.bin diff --git a/cast/streaming/sender_report_parser_fuzzer_seeds/sender_report_without_report_block.bin b/cast/streaming/sender_report_parser_fuzzer_seeds/sender_report_without_report_block.bin Binary files differnew file mode 100644 index 00000000..79ed1909 --- /dev/null +++ b/cast/streaming/sender_report_parser_fuzzer_seeds/sender_report_without_report_block.bin diff --git a/cast/streaming/sender_report_unittest.cc b/cast/streaming/sender_report_unittest.cc new file mode 100644 index 00000000..bad2cf0a --- /dev/null +++ b/cast/streaming/sender_report_unittest.cc @@ -0,0 +1,166 @@ +// 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 <cmath> + +#include "absl/types/span.h" +#include "cast/streaming/sender_report_builder.h" +#include "cast/streaming/sender_report_parser.h" +#include "gtest/gtest.h" + +namespace cast { +namespace streaming { +namespace { + +constexpr Ssrc kSenderSsrc{1}; +constexpr Ssrc kReceiverSsrc{2}; + +class SenderReportTest : public testing::Test { + public: + SenderReportBuilder* builder() { return &builder_; } + SenderReportParser* parser() { return &parser_; } + const NtpTimeConverter& ntp_converter() const { + return session_.ntp_converter(); + } + + private: + RtcpSession session_{kSenderSsrc, kReceiverSsrc, + openscreen::platform::Clock::now()}; + SenderReportBuilder builder_{&session_}; + SenderReportParser parser_{&session_}; +}; + +// Tests that the compound RTCP packets containing a Sender Report alongside +// zero or more other messages can be parsed successfully. +TEST_F(SenderReportTest, Parsing) { + // clang-format off + const uint8_t kSenderReportPacket[] = { + 0b10000001, // Version=2, Padding=no, ItemCount=1 byte. + 200, // RTCP Packet type byte. + 0x00, 0x0c, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x01, // SSRC of sender. + 0xe0, 0x73, 0x2e, 0x54, // NTP Timestamp (late evening on 2019-04-30). + 0x80, 0x00, 0x00, 0x00, + 0x00, 0x14, 0x99, 0x70, // RTP Timestamp (15 seconds, 90kHz timebase). + 0x00, 0x00, 0x01, 0xff, // Sender's Packet Count. + 0x00, 0x07, 0x11, 0x0d, // Sender's Octet Count. + 0x00, 0x00, 0x00, 0x02, // SSRC of receiver (to whom this report is for). + 0x00, // Fraction lost. + 0x00, 0x00, 0x02, // Cumulative Number of Packets Lost. + 0x00, 0x00, 0x38, 0x40, // Highest Sequence Number Received. + 0x00, 0x00, 0x03, 0x84, // Interarrival Jitter. + 0xaf, 0xd3, 0xff, 0x00, // Sender Report ID. + 0x00, 0x00, 0x83, 0xfa, // Delay since last Sender Report. + }; + + constexpr NtpTimestamp kNtpTimestampInSenderReport{0xe0732e5480000000}; + + const uint8_t kOtherPacket[] = { + 0b10000000, // Version=2, Padding=no, ItemCount=0 byte. + 204, // RTCP Packet type byte. + 0x00, 0x01, // Length of remainder of packet, in 32-bit words. + 0x00, 0x00, 0x00, 0x02, // SSRC of receiver. + }; + // clang-format on + + // A RTCP packet only containing non-sender-reports will not provide a Sender + // Report result. + EXPECT_FALSE(parser()->Parse(kOtherPacket)); + + // A compound RTCP packet containing a Sender Report alongside other things + // should be detected as "well-formed" by the parser and it should also + // provide a Sender Report result. Also, it shouldn't matter what the ordering + // is. + const absl::Span<const uint8_t> kCompoundCombinations[2][2] = { + {kSenderReportPacket, kOtherPacket}, + {kOtherPacket, kSenderReportPacket}, + }; + for (const auto& combo : kCompoundCombinations) { + uint8_t compound_packet[sizeof(kSenderReportPacket) + sizeof(kOtherPacket)]; + memcpy(compound_packet, combo[0].data(), combo[0].size()); + memcpy(compound_packet + combo[0].size(), combo[1].data(), combo[1].size()); + + const auto parsed = parser()->Parse(compound_packet); + ASSERT_TRUE(parsed.has_value()); + EXPECT_EQ(ToStatusReportId(kNtpTimestampInSenderReport), parsed->report_id); + EXPECT_EQ(ntp_converter().ToLocalTime(kNtpTimestampInSenderReport), + parsed->reference_time); + EXPECT_EQ(RtpTimeTicks() + RtpTimeDelta::FromTicks(1350000), + parsed->rtp_timestamp); + EXPECT_EQ(uint32_t{0x1ff}, parsed->send_packet_count); + EXPECT_EQ(uint32_t{0x7110d}, parsed->send_octet_count); + ASSERT_TRUE(parsed->report_block.has_value()); + EXPECT_EQ(kReceiverSsrc, parsed->report_block->ssrc); + // Note: RtcpReportBlock parsing is unit-tested elsewhere. + } +} + +// Tests that the SenderReportParser will not try to parse an empty packet. +TEST_F(SenderReportTest, WillNotParseEmptyPacket) { + const uint8_t kEmptyPacket[] = {}; + EXPECT_FALSE(parser()->Parse(absl::Span<const uint8_t>(kEmptyPacket, 0))); +} + +// Tests that the SenderReportParser will not parse anything from garbage data. +TEST_F(SenderReportTest, WillNotParseGarbage) { + // clang-format off + const uint8_t kGarbage[] = { + 0x4f, 0x27, 0xeb, 0x22, 0x27, 0xeb, 0x22, 0x4f, + 0xeb, 0x22, 0x4f, 0x27, 0x22, 0x4f, 0x27, 0xeb, + }; + // clang-format on + EXPECT_FALSE(parser()->Parse(kGarbage)); +} + +// Assuming that SenderReportTest.Parsing has been proven the implementation, +// this test checks that the builder produces RTCP packets that can be parsed. +TEST_F(SenderReportTest, BuildPackets) { + for (int i = 0; i <= 1; ++i) { + const bool with_report_block = (i == 1); + + RtcpSenderReport original; + original.reference_time = openscreen::platform::Clock::now(); + original.rtp_timestamp = RtpTimeTicks() + RtpTimeDelta::FromTicks(5); + original.send_packet_count = 55; + original.send_octet_count = 20044; + if (with_report_block) { + RtcpReportBlock& report_block = original.report_block.emplace(); + report_block.ssrc = kReceiverSsrc; + } + + uint8_t buffer[kRtcpCommonHeaderSize + kRtcpSenderReportSize + + kRtcpReportBlockSize]; + memset(buffer, 0, sizeof(buffer)); + const auto result = builder()->BuildPacket(original, buffer); + ASSERT_TRUE(result.first.data()); + const int expected_packet_size = + sizeof(buffer) - (with_report_block ? 0 : kRtcpReportBlockSize); + EXPECT_EQ(expected_packet_size, static_cast<int>(result.first.size())); + const StatusReportId expected_status_report_id = ToStatusReportId( + ntp_converter().ToNtpTimestamp(original.reference_time)); + EXPECT_EQ(expected_status_report_id, result.second); + + const auto parsed = parser()->Parse(result.first); + ASSERT_TRUE(parsed.has_value()); + EXPECT_EQ(expected_status_report_id, parsed->report_id); + // Note: The reference time can be off by one platform clock tick due to + // a lossy conversion when going to and from the wire-format NtpTimestamps. + // See the unit tests in ntp_time_unittest.cc for further discussion. + EXPECT_LE( + std::abs((original.reference_time - parsed->reference_time).count()), + 1); + EXPECT_EQ(original.rtp_timestamp, parsed->rtp_timestamp); + EXPECT_EQ(original.send_packet_count, parsed->send_packet_count); + EXPECT_EQ(original.send_octet_count, parsed->send_octet_count); + if (with_report_block) { + ASSERT_TRUE(parsed->report_block.has_value()); + EXPECT_EQ(original.report_block->ssrc, parsed->report_block->ssrc); + // Note: RtcpReportBlock serialization/parsing is unit-tested elsewhere. + } + } +} + +} // namespace +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/session_config.cc b/cast/streaming/session_config.cc index f916a18d..a6b09f00 100644 --- a/cast/streaming/session_config.cc +++ b/cast/streaming/session_config.cc @@ -7,8 +7,8 @@ namespace cast { namespace streaming { -SessionConfig::SessionConfig(openscreen::cast_streaming::Ssrc sender_ssrc, - openscreen::cast_streaming::Ssrc receiver_ssrc, +SessionConfig::SessionConfig(Ssrc sender_ssrc, + Ssrc receiver_ssrc, int rtp_timebase, int channels, std::array<uint8_t, 16> aes_secret_key, diff --git a/cast/streaming/session_config.h b/cast/streaming/session_config.h index 3c7bc7d9..d61efc11 100644 --- a/cast/streaming/session_config.h +++ b/cast/streaming/session_config.h @@ -6,11 +6,9 @@ #define CAST_STREAMING_SESSION_CONFIG_H_ #include <array> -#include <chrono> // NOLINT #include <cstdint> -#include <string> -#include "streaming/cast/ssrc.h" +#include "cast/streaming/ssrc.h" namespace cast { namespace streaming { @@ -19,8 +17,8 @@ namespace streaming { // that the Sender and Receiver are both assuming. // TODO(jophba): add config validation. struct SessionConfig final { - SessionConfig(openscreen::cast_streaming::Ssrc sender_ssrc, - openscreen::cast_streaming::Ssrc receiver_ssrc, + SessionConfig(Ssrc sender_ssrc, + Ssrc receiver_ssrc, int rtp_timebase, int channels, std::array<uint8_t, 16> aes_secret_key, @@ -34,8 +32,8 @@ struct SessionConfig final { // The sender and receiver's SSRC identifiers. Note: SSRC identifiers // are defined as unsigned 32 bit integers here: // https://tools.ietf.org/html/rfc5576#page-5 - openscreen::cast_streaming::Ssrc sender_ssrc = 0; - openscreen::cast_streaming::Ssrc receiver_ssrc = 0; + Ssrc sender_ssrc = 0; + Ssrc receiver_ssrc = 0; // RTP timebase: The number of RTP units advanced per second. For audio, // this is the sampling rate. For video, this is 90 kHz by convention. diff --git a/cast/streaming/ssrc.cc b/cast/streaming/ssrc.cc new file mode 100644 index 00000000..d3f24469 --- /dev/null +++ b/cast/streaming/ssrc.cc @@ -0,0 +1,44 @@ +// 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/ssrc.h" + +#include <random> + +#include "platform/api/time.h" + +namespace cast { +namespace streaming { + +namespace { + +// These ranges are arbitrary, but have been used for several years (in prior +// implementations of Cast Streaming). +constexpr int kHigherPriorityMin = 1; +constexpr int kHigherPriorityMax = 50000; +constexpr int kNormalPriorityMin = 50001; +constexpr int kNormalPriorityMax = 100000; + +} // namespace + +Ssrc GenerateSsrc(bool higher_priority) { + // Use a statically-allocated generator, instantiated upon first use, and + // seeded with the current time tick count. This generator was chosen because + // it is light-weight and does not need to produce unguessable (nor + // crypto-secure) values. + static std::minstd_rand generator(static_cast<std::minstd_rand::result_type>( + openscreen::platform::Clock::now().time_since_epoch().count())); + + std::uniform_int_distribution<int> distribution( + higher_priority ? kHigherPriorityMin : kNormalPriorityMin, + higher_priority ? kHigherPriorityMax : kNormalPriorityMax); + return static_cast<Ssrc>(distribution(generator)); +} + +int ComparePriority(Ssrc ssrc_a, Ssrc ssrc_b) { + return static_cast<int>(ssrc_a) - static_cast<int>(ssrc_b); +} + +} // namespace streaming +} // namespace cast diff --git a/cast/streaming/ssrc.h b/cast/streaming/ssrc.h new file mode 100644 index 00000000..0cd23566 --- /dev/null +++ b/cast/streaming/ssrc.h @@ -0,0 +1,39 @@ +// 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. + +#ifndef CAST_STREAMING_SSRC_H_ +#define CAST_STREAMING_SSRC_H_ + +#include <stdint.h> + +namespace cast { +namespace streaming { + +// A Synchronization Source is a 32-bit opaque identifier used in RTP packets +// for identifying the source (or recipient) of a logical sequence of encoded +// audio/video frames. In other words, an audio stream will have one sender SSRC +// and a video stream will have a different sender SSRC. +using Ssrc = uint32_t; + +// The "not set" or "null" value for the Ssrc type. +constexpr Ssrc kNullSsrc{0}; + +// Computes a new SSRC that will be used to uniquely identify an RTP stream. The +// |higher_priority| argument, if true, will generate an SSRC that causes the +// system to use a higher priority when scheduling data transmission. Generally, +// this is set to true for audio streams and false for video streams. +Ssrc GenerateSsrc(bool higher_priority); + +// Returns a value indicating how to prioritize data transmission for a stream +// with |ssrc_a| versus a stream with |ssrc_b|: +// +// ret < 0: Stream |ssrc_a| has higher priority. +// ret == 0: Equal priority. +// ret > 0: Stream |ssrc_b| has higher priority. +int ComparePriority(Ssrc ssrc_a, Ssrc ssrc_b); + +} // namespace streaming +} // namespace cast + +#endif // CAST_STREAMING_SSRC_H_ diff --git a/cast/streaming/ssrc_unittest.cc b/cast/streaming/ssrc_unittest.cc new file mode 100644 index 00000000..29741409 --- /dev/null +++ b/cast/streaming/ssrc_unittest.cc @@ -0,0 +1,57 @@ +// 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/ssrc.h" + +#include <vector> + +#include "gtest/gtest.h" +#include "util/std_util.h" + +using openscreen::SortAndDedupeElements; + +namespace cast { +namespace streaming { +namespace { + +TEST(SsrcTest, GeneratesUniqueAndPrioritizedSsrcs) { + std::vector<Ssrc> priority_ssrcs; + for (int i = 0; i < 3; ++i) { + priority_ssrcs.push_back(GenerateSsrc(true)); + } + + // Three different higher-priority SSRCs should have been generated. + SortAndDedupeElements(&priority_ssrcs); + EXPECT_EQ(3u, priority_ssrcs.size()); + + std::vector<Ssrc> normal_ssrcs; + for (int i = 0; i < 3; ++i) { + normal_ssrcs.push_back(GenerateSsrc(false)); + } + + // Three different normal SSRCs should have been generated. + SortAndDedupeElements(&normal_ssrcs); + EXPECT_EQ(3u, normal_ssrcs.size()); + + // All six SSRCs, together, should be unique. + std::vector<Ssrc> all_ssrcs; + all_ssrcs.insert(all_ssrcs.end(), priority_ssrcs.begin(), + priority_ssrcs.end()); + all_ssrcs.insert(all_ssrcs.end(), normal_ssrcs.begin(), normal_ssrcs.end()); + SortAndDedupeElements(&all_ssrcs); + EXPECT_EQ(6u, all_ssrcs.size()); + + // ComparePriority() should return values indicating the appropriate + // prioritization. + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + EXPECT_LT(ComparePriority(priority_ssrcs[i], normal_ssrcs[j]), 0); + EXPECT_GT(ComparePriority(normal_ssrcs[i], priority_ssrcs[j]), 0); + } + } +} + +} // namespace +} // namespace streaming +} // namespace cast |