aboutsummaryrefslogtreecommitdiff
path: root/cast/streaming
diff options
context:
space:
mode:
authorYuri Wiitala <miu@chromium.org>2019-11-22 15:04:27 -0800
committerCommit Bot <commit-bot@chromium.org>2019-11-22 23:17:56 +0000
commit7d2583a9952b389760fbec7c9eed515bb2270fb4 (patch)
tree6929ab25ae7972e283eb866d04be9c0ef7583815 /cast/streaming
parentef98b330b71a64ef3a427b93fcfa92a76c06a597 (diff)
downloadopenscreen-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')
-rw-r--r--cast/streaming/BUILD.gn183
-rw-r--r--cast/streaming/DEPS11
-rw-r--r--cast/streaming/clock_drift_smoother.cc72
-rw-r--r--cast/streaming/clock_drift_smoother.h60
-rw-r--r--cast/streaming/compound_rtcp_builder.cc332
-rw-r--r--cast/streaming/compound_rtcp_builder.h140
-rw-r--r--cast/streaming/compound_rtcp_builder_unittest.cc373
-rw-r--r--cast/streaming/compound_rtcp_parser.cc381
-rw-r--r--cast/streaming/compound_rtcp_parser.h122
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer.cc51
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_basics.binbin0 -> 56 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_including_picture_loss_indicator.binbin0 -> 68 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_including_receiver_report_block.binbin0 -> 80 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_lots_of_nacks.binbin0 -> 256 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_lots_of_nacks_and_some_acks.binbin0 -> 256 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_lots_of_nacks_and_some_more_acks.binbin0 -> 256 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_multiple_acks.binbin0 -> 60 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_nack_mix.binbin0 -> 76 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_one_ack.binbin0 -> 56 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/feedback_with_nacks.binbin0 -> 48 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/feedback_with_one_nack_and_one_ack.binbin0 -> 32 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/feedback_with_zero_nacks_and_many_acks.binbin0 -> 36 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/picture_loss_indicator.binbin0 -> 12 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/picture_loss_indicator_and_reference_time_report.binbin0 -> 32 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/receiver_report_and_reference_time_report.binbin0 -> 28 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/receiver_report_with_report_block.binbin0 -> 32 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/receiver_report_without_report_block.binbin0 -> 8 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/simple_feedback.binbin0 -> 20 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_fuzzer_seeds/three_extended_reports.binbin0 -> 52 bytes
-rw-r--r--cast/streaming/compound_rtcp_parser_unittest.cc408
-rw-r--r--cast/streaming/constants.h44
-rw-r--r--cast/streaming/encoded_frame.cc26
-rw-r--r--cast/streaming/encoded_frame.h99
-rw-r--r--cast/streaming/environment.cc140
-rw-r--r--cast/streaming/environment.h131
-rw-r--r--cast/streaming/expanded_value_base.h164
-rw-r--r--cast/streaming/expanded_value_base_unittest.cc108
-rw-r--r--cast/streaming/frame_collector.cc160
-rw-r--r--cast/streaming/frame_collector.h90
-rw-r--r--cast/streaming/frame_collector_unittest.cc211
-rw-r--r--cast/streaming/frame_crypto.cc120
-rw-r--r--cast/streaming/frame_crypto.h97
-rw-r--r--cast/streaming/frame_crypto_unittest.cc80
-rw-r--r--cast/streaming/frame_id.cc18
-rw-r--r--cast/streaming/frame_id.h110
-rw-r--r--cast/streaming/mock_compound_rtcp_parser_client.h29
-rw-r--r--cast/streaming/ntp_time.cc58
-rw-r--r--cast/streaming/ntp_time.h78
-rw-r--r--cast/streaming/ntp_time_unittest.cc110
-rw-r--r--cast/streaming/packet_receive_stats_tracker.cc81
-rw-r--r--cast/streaming/packet_receive_stats_tracker.h107
-rw-r--r--cast/streaming/packet_receive_stats_tracker_unittest.cc210
-rw-r--r--cast/streaming/packet_util.cc44
-rw-r--r--cast/streaming/packet_util.h62
-rw-r--r--cast/streaming/packet_util_unittest.cc185
-rw-r--r--cast/streaming/receiver.cc485
-rw-r--r--cast/streaming/receiver.h351
-rw-r--r--cast/streaming/receiver_packet_router.cc119
-rw-r--r--cast/streaming/receiver_packet_router.h66
-rw-r--r--cast/streaming/receiver_session.h8
-rw-r--r--cast/streaming/receiver_unittest.cc843
-rw-r--r--cast/streaming/rtcp_common.cc246
-rw-r--r--cast/streaming/rtcp_common.h184
-rw-r--r--cast/streaming/rtcp_common_unittest.cc312
-rw-r--r--cast/streaming/rtcp_session.cc26
-rw-r--r--cast/streaming/rtcp_session.h42
-rw-r--r--cast/streaming/rtp_defines.cc46
-rw-r--r--cast/streaming/rtp_defines.h344
-rw-r--r--cast/streaming/rtp_packet_parser.cc118
-rw-r--r--cast/streaming/rtp_packet_parser.h78
-rw-r--r--cast/streaming/rtp_packet_parser_fuzzer.cc32
-rw-r--r--cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame.bin4
-rw-r--r--cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame_with_bad_packet_id.binbin0 -> 26 bytes
-rw-r--r--cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame_with_latency_ext.binbin0 -> 38 bytes
-rw-r--r--cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_key_frame_with_multiple_ext.binbin0 -> 49 bytes
-rw-r--r--cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_non_key_frame_with_rfid.binbin0 -> 34 bytes
-rw-r--r--cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_for_non_key_frame_without_rfid.binbin0 -> 33 bytes
-rw-r--r--cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_18_bytes.binbin0 -> 18 bytes
-rw-r--r--cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_1_byte.bin1
-rw-r--r--cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_22_bytes.binbin0 -> 22 bytes
-rw-r--r--cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_33_bytes.binbin0 -> 33 bytes
-rw-r--r--cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_34_bytes.binbin0 -> 34 bytes
-rw-r--r--cast/streaming/rtp_packet_parser_unittest.cc313
-rw-r--r--cast/streaming/rtp_packetizer.cc139
-rw-r--r--cast/streaming/rtp_packetizer.h81
-rw-r--r--cast/streaming/rtp_packetizer_unittest.cc207
-rw-r--r--cast/streaming/rtp_time.cc25
-rw-r--r--cast/streaming/rtp_time.h268
-rw-r--r--cast/streaming/rtp_time_unittest.cc75
-rw-r--r--cast/streaming/sender_report_builder.cc56
-rw-r--r--cast/streaming/sender_report_builder.h45
-rw-r--r--cast/streaming/sender_report_parser.cc75
-rw-r--r--cast/streaming/sender_report_parser.h51
-rw-r--r--cast/streaming/sender_report_parser_fuzzer.cc48
-rw-r--r--cast/streaming/sender_report_parser_fuzzer_seeds/compound_packet_with_other_then_sr.binbin0 -> 60 bytes
-rw-r--r--cast/streaming/sender_report_parser_fuzzer_seeds/compound_packet_with_sr_then_other.binbin0 -> 60 bytes
-rw-r--r--cast/streaming/sender_report_parser_fuzzer_seeds/rtcp_packet_with_no_sender_report.binbin0 -> 8 bytes
-rw-r--r--cast/streaming/sender_report_parser_fuzzer_seeds/sender_report_with_report_block.binbin0 -> 52 bytes
-rw-r--r--cast/streaming/sender_report_parser_fuzzer_seeds/sender_report_without_report_block.binbin0 -> 28 bytes
-rw-r--r--cast/streaming/sender_report_unittest.cc166
-rw-r--r--cast/streaming/session_config.cc4
-rw-r--r--cast/streaming/session_config.h12
-rw-r--r--cast/streaming/ssrc.cc44
-rw-r--r--cast/streaming/ssrc.h39
-rw-r--r--cast/streaming/ssrc_unittest.cc57
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
new file mode 100644
index 00000000..9ab122b5
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_basics.bin
Binary files differ
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
new file mode 100644
index 00000000..47a2cc9c
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_including_picture_loss_indicator.bin
Binary files differ
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
new file mode 100644
index 00000000..d9d0a544
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_including_receiver_report_block.bin
Binary files differ
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
new file mode 100644
index 00000000..e3f89d66
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_lots_of_nacks.bin
Binary files differ
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
new 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
Binary files differ
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
new 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
Binary files differ
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
new file mode 100644
index 00000000..59fd6c98
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_multiple_acks.bin
Binary files differ
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
new file mode 100644
index 00000000..005cd37c
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_nack_mix.bin
Binary files differ
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
new file mode 100644
index 00000000..501338a8
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/builder_with_one_ack.bin
Binary files differ
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
new file mode 100644
index 00000000..b2689bdd
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/feedback_with_nacks.bin
Binary files differ
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
new file mode 100644
index 00000000..a41a9938
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/feedback_with_one_nack_and_one_ack.bin
Binary files differ
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
new file mode 100644
index 00000000..7491fd64
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/feedback_with_zero_nacks_and_many_acks.bin
Binary files differ
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
new file mode 100644
index 00000000..2c088ee1
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/picture_loss_indicator.bin
Binary files differ
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
new file mode 100644
index 00000000..edbc6cf0
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/picture_loss_indicator_and_reference_time_report.bin
Binary files differ
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
new file mode 100644
index 00000000..e9dc8b41
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/receiver_report_and_reference_time_report.bin
Binary files differ
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
new file mode 100644
index 00000000..f67d4cc3
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/receiver_report_with_report_block.bin
Binary files differ
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
new file mode 100644
index 00000000..04250612
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/receiver_report_without_report_block.bin
Binary files differ
diff --git a/cast/streaming/compound_rtcp_parser_fuzzer_seeds/simple_feedback.bin b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/simple_feedback.bin
new file mode 100644
index 00000000..8e118ece
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/simple_feedback.bin
Binary files differ
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
new file mode 100644
index 00000000..c513bb2f
--- /dev/null
+++ b/cast/streaming/compound_rtcp_parser_fuzzer_seeds/three_extended_reports.bin
Binary files differ
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
new 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
Binary files differ
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
new 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
Binary files differ
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
new 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
Binary files differ
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
new 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
Binary files differ
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
new 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
Binary files differ
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
new file mode 100644
index 00000000..6b40319f
--- /dev/null
+++ b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_18_bytes.bin
Binary files differ
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
new file mode 100644
index 00000000..e6bff22e
--- /dev/null
+++ b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_22_bytes.bin
Binary files differ
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
new file mode 100644
index 00000000..a50280d6
--- /dev/null
+++ b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_33_bytes.bin
Binary files differ
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
new file mode 100644
index 00000000..66364dae
--- /dev/null
+++ b/cast/streaming/rtp_packet_parser_fuzzer_seeds/rtp_packet_trunc_to_34_bytes.bin
Binary files differ
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
new file mode 100644
index 00000000..9ff47886
--- /dev/null
+++ b/cast/streaming/sender_report_parser_fuzzer_seeds/compound_packet_with_other_then_sr.bin
Binary files differ
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
new file mode 100644
index 00000000..646db5aa
--- /dev/null
+++ b/cast/streaming/sender_report_parser_fuzzer_seeds/compound_packet_with_sr_then_other.bin
Binary files differ
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
new file mode 100644
index 00000000..a5c35e0a
--- /dev/null
+++ b/cast/streaming/sender_report_parser_fuzzer_seeds/rtcp_packet_with_no_sender_report.bin
Binary files differ
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
new file mode 100644
index 00000000..eb4b0d80
--- /dev/null
+++ b/cast/streaming/sender_report_parser_fuzzer_seeds/sender_report_with_report_block.bin
Binary files differ
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
new file mode 100644
index 00000000..79ed1909
--- /dev/null
+++ b/cast/streaming/sender_report_parser_fuzzer_seeds/sender_report_without_report_block.bin
Binary files differ
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