| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "cast/streaming/impl/sender_impl.h" |
| |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <array> |
| #include <chrono> |
| #include <limits> |
| #include <map> |
| #include <set> |
| #include <utility> |
| #include <vector> |
| |
| #include "cast/streaming/impl/compound_rtcp_builder.h" |
| #include "cast/streaming/impl/frame_collector.h" |
| #include "cast/streaming/impl/frame_crypto.h" |
| #include "cast/streaming/impl/packet_util.h" |
| #include "cast/streaming/impl/rtcp_session.h" |
| #include "cast/streaming/impl/rtp_defines.h" |
| #include "cast/streaming/impl/rtp_packet_parser.h" |
| #include "cast/streaming/impl/sender_report_parser.h" |
| #include "cast/streaming/public/constants.h" |
| #include "cast/streaming/public/encoded_frame.h" |
| #include "cast/streaming/public/frame_id.h" |
| #include "cast/streaming/public/session_config.h" |
| #include "cast/streaming/sender_packet_router.h" |
| #include "cast/streaming/ssrc.h" |
| #include "cast/streaming/testing/mock_environment.h" |
| #include "cast/streaming/testing/simple_socket_subscriber.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "platform/base/span.h" |
| #include "platform/test/fake_clock.h" |
| #include "platform/test/fake_task_runner.h" |
| #include "util/alarm.h" |
| #include "util/bit_vector.h" |
| #include "util/chrono_helpers.h" |
| #include "util/raw_ref.h" |
| #include "util/std_util.h" |
| |
| using testing::_; |
| using testing::AtLeast; |
| using testing::InvokeWithoutArgs; |
| using testing::Mock; |
| using testing::NiceMock; |
| using testing::Return; |
| using testing::Sequence; |
| using testing::StrictMock; |
| |
| namespace openscreen::cast { |
| namespace { |
| |
| // Sender configuration. |
| constexpr Ssrc kSenderSsrc = 1; |
| constexpr Ssrc kReceiverSsrc = 2; |
| constexpr int kRtpTimebase = 48000; |
| constexpr milliseconds kTargetPlayoutDelay(400); |
| |
| // The minimum in-flight media duration the Sender enforces regardless of the |
| // measured round-trip time. Must match kMinSenderInFlight in sender_impl.cc. |
| constexpr Clock::duration kMinSenderInFlight = milliseconds(66); |
| 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 RtpPayloadType kRtpPayloadType = RtpPayloadType::kVideoVp8; |
| |
| // The number of RTP ticks advanced per frame, for 100 FPS media. |
| constexpr int kRtpTicksPerFrame = kRtpTimebase / 100; |
| |
| // The number of milliseconds advanced per frame, for 100 FPS media. |
| constexpr milliseconds kFrameDuration{1000 / 100}; |
| static_assert(kFrameDuration < (kTargetPlayoutDelay / 10), |
| "Kickstart test assumes frame duration is far less than the " |
| "playout delay."); |
| |
| // An Encoded frame that also holds onto its own copy of data. |
| struct EncodedFrameWithBuffer : public EncodedFrame { |
| // `EncodedFrame::data` always points inside buffer.begin()...buffer.end(). |
| std::vector<uint8_t> buffer; |
| }; |
| |
| // SenderPacketRouter configuration for these tests. |
| constexpr int kNumPacketsPerBurst = 20; |
| constexpr milliseconds kBurstInterval(10); |
| |
| // An arbitrary value, subtracted from "now," to specify the reference_time on |
| // frames that are about to be enqueued. This simulates that capture+encode |
| // happened in the past, before Sender::EnqueueFrame() is called. |
| constexpr milliseconds kCaptureDelay(11); |
| |
| // In some tests, the computed time values could be off a little bit due to |
| // imprecision in certain wire-format timestamp types. The following macro |
| // behaves just like Gtest's EXPECT_NEAR(), but works with all the time types |
| // too. |
| #define EXPECT_NEARLY_EQUAL(duration_a, duration_b, epsilon) \ |
| if ((duration_a) >= (duration_b)) { \ |
| EXPECT_LE((duration_a), (duration_b) + (epsilon)); \ |
| } else { \ |
| EXPECT_GE((duration_a), (duration_b) - (epsilon)); \ |
| } |
| |
| void OverrideRtpTimestamp(int frame_count, EncodedFrame* frame, int fps) { |
| const int ticks = frame_count * kRtpTimebase / fps; |
| frame->rtp_timestamp = RtpTimeTicks() + RtpTimeDelta::FromTicks(ticks); |
| } |
| |
| // Simulates UDP/IPv6 traffic in one direction (from Sender→Receiver, or |
| // Receiver→Sender), with a settable amount of delay. |
| class SimulatedNetworkPipe { |
| public: |
| SimulatedNetworkPipe(TaskRunner& task_runner, |
| Environment::PacketConsumer& remote) |
| : task_runner_(task_runner), remote_(remote) { |
| // Create a fake IPv6 address using the "documentative purposes" prefix |
| // concatenated with the `this` pointer. |
| std::array<uint16_t, 8> hextets{}; |
| hextets[0] = 0x2001; |
| hextets[1] = 0x0db8; |
| auto* const this_pointer = this; |
| static_assert(sizeof(this_pointer) <= (6 * sizeof(uint16_t)), ""); |
| memcpy(&hextets[2], &this_pointer, sizeof(this_pointer)); |
| local_endpoint_ = IPEndpoint{IPAddress(hextets), 2344}; |
| } |
| |
| const IPEndpoint& local_endpoint() const { return local_endpoint_; } |
| |
| Clock::duration network_delay() const { return network_delay_; } |
| void set_network_delay(Clock::duration delay) { network_delay_ = delay; } |
| |
| // The caller needs to spin the task runner before `packet` will reach the |
| // other side. |
| void StartPacketTransmission(std::vector<uint8_t> packet) { |
| task_runner_->PostTaskWithDelay( |
| [this, pkt = std::move(packet)]() mutable { |
| remote_->OnReceivedPacket(local_endpoint_, FakeClock::now(), |
| std::move(pkt)); |
| }, |
| network_delay_); |
| } |
| |
| private: |
| const raw_ref<TaskRunner> task_runner_; |
| const raw_ref<Environment::PacketConsumer> remote_; |
| |
| IPEndpoint local_endpoint_; |
| |
| // The amount of time for the packet to transmit over this simulated network |
| // pipe. Defaults to zero to simplify the tests that don't care about delays. |
| Clock::duration network_delay_{}; |
| }; |
| |
| // Processes packets from the Sender under test, allowing unit tests to set |
| // expectations for parsed RTP or RTCP packets, to confirm proper behavior of |
| // the Sender. |
| class MockReceiver : public Environment::PacketConsumer { |
| public: |
| explicit MockReceiver(SimulatedNetworkPipe& pipe_to_sender) |
| : pipe_to_sender_(pipe_to_sender), |
| rtcp_session_(kSenderSsrc, kReceiverSsrc, FakeClock::now()), |
| sender_report_parser_(rtcp_session_), |
| rtcp_builder_(rtcp_session_), |
| rtp_parser_(kSenderSsrc), |
| crypto_(kAesKey, kCastIvMask) { |
| rtcp_builder_.SetPlayoutDelay(kTargetPlayoutDelay); |
| } |
| |
| ~MockReceiver() override = default; |
| |
| // Simulate the Receiver ACK'ing all frames up to and including the |
| // `new_checkpoint`. |
| void SetCheckpointFrame(FrameId new_checkpoint) { |
| OSP_CHECK_GE(new_checkpoint, rtcp_builder_.checkpoint_frame()); |
| rtcp_builder_.SetCheckpointFrame(new_checkpoint); |
| } |
| |
| // Automatically advances the checkpoint based on what is found in |
| // `complete_frames_`, returning true if the checkpoint moved forward. |
| bool AutoAdvanceCheckpoint() { |
| const FrameId old_checkpoint = rtcp_builder_.checkpoint_frame(); |
| FrameId new_checkpoint = old_checkpoint; |
| for (auto it = complete_frames_.upper_bound(old_checkpoint); |
| it != complete_frames_.end(); ++it) { |
| if (it->first != new_checkpoint + 1) { |
| break; |
| } |
| ++new_checkpoint; |
| } |
| if (new_checkpoint > old_checkpoint) { |
| rtcp_builder_.SetCheckpointFrame(new_checkpoint); |
| return true; |
| } |
| return false; |
| } |
| |
| void SetPictureLossIndicator(bool picture_is_lost) { |
| rtcp_builder_.SetPictureLossIndicator(picture_is_lost); |
| } |
| |
| void SetReceiverReport(StatusReportId reply_for, |
| RtcpReportBlock::Delay processing_delay) { |
| RtcpReportBlock receiver_report; |
| receiver_report.ssrc = kSenderSsrc; |
| receiver_report.last_status_report_id = reply_for; |
| receiver_report.delay_since_last_report = processing_delay; |
| rtcp_builder_.IncludeReceiverReportInNextPacket(receiver_report); |
| } |
| |
| void SetNacksAndAcks(std::vector<PacketNack> packet_nacks, |
| std::vector<FrameId> frame_acks) { |
| rtcp_builder_.IncludeFeedbackInNextPacket(std::move(packet_nacks), |
| std::move(frame_acks)); |
| } |
| |
| // Builds and sends a RTCP packet containing one or more of: checkpoint, PLI, |
| // Receiver Report, NACKs, ACKs. |
| void TransmitRtcpFeedbackPacket() { |
| uint8_t buffer[kMaxRtpPacketSizeForIpv6UdpOnEthernet]; |
| const ByteBuffer packet = |
| rtcp_builder_.BuildPacket(FakeClock::now(), buffer); |
| pipe_to_sender_->StartPacketTransmission( |
| std::vector<uint8_t>(packet.begin(), packet.end())); |
| } |
| |
| // Used by tests to simulate the Receiver not seeing specific packets come in |
| // from the network (e.g., because the network dropped the packets). |
| void SetIgnoreList(std::vector<PacketNack> ignore_list) { |
| ignore_list_ = ignore_list; |
| } |
| |
| // Environment::PacketConsumer implementation. |
| // |
| // Called to process a packet from the Sender, simulating basic RTP frame |
| // collection and Sender Report parsing/handling. |
| void OnReceivedPacket(const IPEndpoint& source, |
| Clock::time_point arrival_time, |
| std::vector<uint8_t> packet) override { |
| const auto type_and_ssrc = InspectPacketForRouting(packet); |
| EXPECT_NE(ApparentPacketType::UNKNOWN, type_and_ssrc.first); |
| EXPECT_EQ(kSenderSsrc, type_and_ssrc.second); |
| if (type_and_ssrc.first == ApparentPacketType::RTP) { |
| const std::optional<RtpPacketParser::ParseResult> part_of_frame = |
| rtp_parser_.Parse(packet); |
| ASSERT_TRUE(part_of_frame); |
| |
| // Return early if simulating packet drops over the network. |
| if (ContainsIf(ignore_list_, [&](const PacketNack& baddie) { |
| return (baddie.frame_id == part_of_frame->frame_id && |
| (baddie.packet_id == kAllPacketsLost || |
| baddie.packet_id == part_of_frame->packet_id)); |
| })) { |
| return; |
| } |
| |
| OnRtpPacket(*part_of_frame); |
| CollectRtpPacket(*part_of_frame, std::move(packet)); |
| } else if (type_and_ssrc.first == ApparentPacketType::RTCP) { |
| std::optional<SenderReportParser::SenderReportWithId> report = |
| sender_report_parser_.Parse(packet); |
| ASSERT_TRUE(report); |
| OnSenderReport(*report); |
| } |
| } |
| |
| std::map<FrameId, EncodedFrameWithBuffer> TakeCompleteFrames() { |
| std::map<FrameId, EncodedFrameWithBuffer> result; |
| result.swap(complete_frames_); |
| return result; |
| } |
| |
| // Tests set expectations on these mocks to monitor events of interest, and/or |
| // invoke additional behaviors. |
| MOCK_METHOD1(OnRtpPacket, |
| void(const RtpPacketParser::ParseResult& parsed_packet)); |
| MOCK_METHOD1(OnFrameComplete, void(FrameId frame_id)); |
| MOCK_METHOD1(OnSenderReport, |
| void(const SenderReportParser::SenderReportWithId& report)); |
| |
| private: |
| // Collects the individual RTP packets until a whole frame can be formed, then |
| // calls OnFrameComplete(). Ignores extra RTP packets that are no longer |
| // needed. |
| void CollectRtpPacket(const RtpPacketParser::ParseResult& part_of_frame, |
| std::vector<uint8_t> packet) { |
| const FrameId frame_id = part_of_frame.frame_id; |
| if (complete_frames_.find(frame_id) != complete_frames_.end()) { |
| return; |
| } |
| FrameCollector& collector = incomplete_frames_[frame_id]; |
| collector.set_frame_id(frame_id); |
| EXPECT_TRUE(collector.CollectRtpPacket(part_of_frame, &packet)); |
| if (!collector.is_complete()) { |
| return; |
| } |
| const EncodedFrame& metadata = collector.PeekFrameMetadata(); |
| const size_t payload_size = collector.GetFramePayloadSize(); |
| EncodedFrameWithBuffer& decrypted = complete_frames_[frame_id]; |
| // Note: Not setting decrypted->reference_time here since the logic around |
| // calculating the playout time is rather complex, and is definitely outside |
| // the scope of the testing being done in this module. Instead, end-to-end |
| // testing should exist elsewhere to confirm frame play-out times with real |
| // Receivers. |
| decrypted.buffer.resize(payload_size); |
| crypto_.Decrypt(metadata.frame_id, collector.GetPayloadChunks(), |
| decrypted.buffer); |
| metadata.CopyMetadataTo(&decrypted); |
| decrypted.data = decrypted.buffer; |
| incomplete_frames_.erase(frame_id); |
| OnFrameComplete(frame_id); |
| } |
| |
| const raw_ref<SimulatedNetworkPipe> pipe_to_sender_; |
| RtcpSession rtcp_session_; |
| SenderReportParser sender_report_parser_; |
| CompoundRtcpBuilder rtcp_builder_; |
| RtpPacketParser rtp_parser_; |
| FrameCrypto crypto_; |
| |
| std::vector<PacketNack> ignore_list_; |
| std::map<FrameId, FrameCollector> incomplete_frames_; |
| std::map<FrameId, EncodedFrameWithBuffer> complete_frames_; |
| }; |
| |
| class MockObserver : public Sender::Observer { |
| public: |
| MOCK_METHOD1(OnFrameCanceled, void(FrameId frame_id)); |
| MOCK_METHOD0(OnPictureLost, void()); |
| }; |
| |
| class SenderTest : public testing::Test { |
| public: |
| SenderTest() |
| : fake_clock_(Clock::now()), |
| task_runner_(fake_clock_), |
| sender_environment_(&FakeClock::now, task_runner_), |
| sender_packet_router_(sender_environment_, |
| kNumPacketsPerBurst, |
| kBurstInterval), |
| sender_(sender_environment_, |
| sender_packet_router_, |
| {/* .sender_ssrc = */ kSenderSsrc, |
| /* .receiver_ssrc = */ kReceiverSsrc, |
| /* .rtp_timebase = */ kRtpTimebase, |
| /* .channels = */ 2, |
| /* .target_playout_delay = */ kTargetPlayoutDelay, |
| /* .aes_secret_key = */ kAesKey, |
| /* .aes_iv_mask = */ kCastIvMask, |
| /* .is_pli_enabled = */ true}, |
| kRtpPayloadType), |
| receiver_to_sender_pipe_(task_runner_, sender_packet_router_), |
| receiver_(receiver_to_sender_pipe_), |
| sender_to_receiver_pipe_(task_runner_, receiver_) { |
| sender_environment_.SetSocketSubscriber(&socket_subscriber_); |
| sender_environment_.set_remote_endpoint( |
| receiver_to_sender_pipe_.local_endpoint()); |
| ON_CALL(sender_environment_, SendPacket(_, _)) |
| .WillByDefault([this](ByteView packet, PacketMetadata metadata) { |
| sender_to_receiver_pipe_.StartPacketTransmission( |
| std::vector<uint8_t>(packet.begin(), packet.end())); |
| }); |
| } |
| |
| ~SenderTest() override = default; |
| |
| SenderImpl* sender() { return &sender_; } |
| MockReceiver* receiver() { return &receiver_; } |
| |
| void SetReceiverToSenderNetworkDelay(Clock::duration delay) { |
| receiver_to_sender_pipe_.set_network_delay(delay); |
| } |
| |
| void SetSenderToReceiverNetworkDelay(Clock::duration delay) { |
| sender_to_receiver_pipe_.set_network_delay(delay); |
| } |
| |
| void SimulateExecution(Clock::duration how_long = Clock::duration::zero()) { |
| fake_clock_.Advance(how_long); |
| } |
| |
| // Simulates one Sender→Receiver→Sender RTCP round-trip using the given |
| // one-way network delays, causing the Sender to (re)measure the network |
| // round-trip time. A frame must already be in-flight so the Sender emits a |
| // Sender Report. |
| void SimulateNetworkRoundTrip(milliseconds outbound, milliseconds inbound) { |
| SetSenderToReceiverNetworkDelay(outbound); |
| SetReceiverToSenderNetworkDelay(inbound); |
| |
| StatusReportId sender_report_id{}; |
| EXPECT_CALL(*receiver(), OnSenderReport(_)) |
| .WillOnce([&](const SenderReportParser::SenderReportWithId& report) { |
| sender_report_id = report.report_id; |
| }); |
| // The Sender Report reaches the Receiver after the outbound delay. |
| SimulateExecution(outbound); |
| Mock::VerifyAndClearExpectations(receiver()); |
| ASSERT_NE(StatusReportId{}, sender_report_id); |
| // The Receiver spends some time before replying. This delay is included in |
| // the Receiver Report so that the Sender can isolate the network delay. |
| constexpr milliseconds kReceiverProcessingDelay(2); |
| SimulateExecution(kReceiverProcessingDelay); |
| receiver()->SetReceiverReport( |
| sender_report_id, std::chrono::duration_cast<RtcpReportBlock::Delay>( |
| kReceiverProcessingDelay)); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| // The Receiver Report travels back to the Sender after the inbound delay. |
| SimulateExecution(inbound); |
| } |
| |
| // Constructs a Sender with the given target playout delay (and no measured |
| // round-trip time) and returns its maximum in-flight media duration. Uses |
| // distinct SSRCs so it does not collide with the fixture's Sender. |
| Clock::duration MaxInFlightForPlayoutDelay( |
| milliseconds target_playout_delay) { |
| SenderImpl sender(sender_environment_, sender_packet_router_, |
| {/* .sender_ssrc = */ kSenderSsrc + 100, |
| /* .receiver_ssrc = */ kReceiverSsrc + 100, |
| /* .rtp_timebase = */ kRtpTimebase, |
| /* .channels = */ 2, |
| /* .target_playout_delay = */ target_playout_delay, |
| /* .aes_secret_key = */ kAesKey, |
| /* .aes_iv_mask = */ kCastIvMask, |
| /* .is_pli_enabled = */ true}, |
| kRtpPayloadType); |
| return sender.GetMaxInFlightMediaDuration(); |
| } |
| |
| static void PopulateFramePayloadBuffer(int seed, |
| int num_bytes, |
| std::vector<uint8_t>* payload) { |
| payload->clear(); |
| payload->reserve(num_bytes); |
| for (int i = 0; i < num_bytes; ++i) { |
| payload->push_back(static_cast<uint8_t>(seed + i)); |
| } |
| } |
| |
| static void PopulateFrameWithDefaults(FrameId frame_id, |
| Clock::time_point reference_time, |
| int seed, |
| int num_payload_bytes, |
| EncodedFrameWithBuffer* frame) { |
| frame->dependency = (frame_id == FrameId::first()) |
| ? EncodedFrame::Dependency::kKeyFrame |
| : EncodedFrame::Dependency::kDependent; |
| frame->frame_id = frame_id; |
| frame->referenced_frame_id = frame->frame_id; |
| if (frame_id != FrameId::first()) { |
| --frame->referenced_frame_id; |
| } |
| frame->rtp_timestamp = |
| RtpTimeTicks() + (RtpTimeDelta::FromTicks(kRtpTicksPerFrame) * |
| (frame_id - FrameId::first())); |
| frame->reference_time = reference_time; |
| PopulateFramePayloadBuffer(seed, num_payload_bytes, &frame->buffer); |
| frame->data = frame->buffer; |
| } |
| |
| // Confirms that all `sent_frames` exist in `received_frames`, with identical |
| // data and metadata. |
| static void ExpectFramesReceivedCorrectly( |
| Span<EncodedFrameWithBuffer> sent_frames, |
| const std::map<FrameId, EncodedFrameWithBuffer> received_frames) { |
| ASSERT_EQ(sent_frames.size(), received_frames.size()); |
| |
| for (const EncodedFrameWithBuffer& sent_frame : sent_frames) { |
| SCOPED_TRACE(testing::Message() |
| << "Checking sent frame " << sent_frame.frame_id); |
| const auto received_it = received_frames.find(sent_frame.frame_id); |
| if (received_it == received_frames.end()) { |
| ADD_FAILURE() << "Did not receive frame."; |
| continue; |
| } |
| const EncodedFrame& received_frame = received_it->second; |
| EXPECT_EQ(sent_frame.dependency, received_frame.dependency); |
| EXPECT_EQ(sent_frame.referenced_frame_id, |
| received_frame.referenced_frame_id); |
| EXPECT_EQ(sent_frame.rtp_timestamp, received_frame.rtp_timestamp); |
| EXPECT_THAT(sent_frame.data, |
| testing::ElementsAreArray(received_frame.data)); |
| } |
| } |
| |
| private: |
| FakeClock fake_clock_; |
| FakeTaskRunner task_runner_; |
| NiceMock<MockEnvironment> sender_environment_; |
| SenderPacketRouter sender_packet_router_; |
| SenderImpl sender_; |
| SimulatedNetworkPipe receiver_to_sender_pipe_; |
| NiceMock<MockReceiver> receiver_; |
| SimulatedNetworkPipe sender_to_receiver_pipe_; |
| SimpleSubscriber socket_subscriber_; |
| }; |
| |
| // Tests that the Sender can send EncodedFrames over an ideal network (i.e., low |
| // latency, no loss), and does so without having to transmit the same packet |
| // twice. |
| TEST_F(SenderTest, SendsFramesEfficiently) { |
| constexpr milliseconds kOneWayNetworkDelay(1); |
| SetSenderToReceiverNetworkDelay(kOneWayNetworkDelay); |
| SetReceiverToSenderNetworkDelay(kOneWayNetworkDelay); |
| |
| // Expect that each packet is only sent once. |
| std::set<std::pair<FrameId, FramePacketId>> received_packets; |
| EXPECT_CALL(*receiver(), OnRtpPacket(_)) |
| .WillRepeatedly([&](const RtpPacketParser::ParseResult& parsed_packet) { |
| std::pair<FrameId, FramePacketId> id(parsed_packet.frame_id, |
| parsed_packet.packet_id); |
| const auto insert_result = received_packets.insert(id); |
| EXPECT_TRUE(insert_result.second) |
| << "Received duplicate packet: " << id.first << ':' |
| << static_cast<int>(id.second); |
| }); |
| |
| // Simulate normal frame ACK'ing behavior. |
| ON_CALL(*receiver(), OnFrameComplete(_)).WillByDefault(InvokeWithoutArgs([&] { |
| if (receiver()->AutoAdvanceCheckpoint()) { |
| receiver()->TransmitRtcpFeedbackPacket(); |
| } |
| })); |
| |
| StrictMock<MockObserver> observer; |
| EXPECT_CALL(observer, OnFrameCanceled(FrameId::first())); |
| EXPECT_CALL(observer, OnFrameCanceled(FrameId::first() + 1)); |
| EXPECT_CALL(observer, OnFrameCanceled(FrameId::first() + 2)); |
| sender()->SetObserver(&observer); |
| |
| EncodedFrameWithBuffer frames[3]; |
| constexpr int kFrameDataSizes[] = {8196, 12, 1900}; |
| for (int i = 0; i < 3; ++i) { |
| if (i == 0) { |
| EXPECT_TRUE(sender()->NeedsKeyFrame()); |
| } else { |
| EXPECT_FALSE(sender()->NeedsKeyFrame()); |
| } |
| PopulateFrameWithDefaults(FrameId::first() + i, |
| FakeClock::now() - kCaptureDelay, 0xbf - i, |
| kFrameDataSizes[i], &frames[i]); |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(frames[i])); |
| SimulateExecution(kFrameDuration); |
| } |
| SimulateExecution(kTargetPlayoutDelay); |
| |
| ExpectFramesReceivedCorrectly(frames, receiver()->TakeCompleteFrames()); |
| } |
| |
| // Tests that the Sender properly updates the checkpoint frame ID while |
| // it is cancelling frames. See https://crbug.com/1433584 for an example crash |
| // where the checkpoint frame ID is invalid. |
| TEST_F(SenderTest, WaitsUntilEndOfReportToUpdateObservers) { |
| constexpr milliseconds kOneWayNetworkDelay(1); |
| SetSenderToReceiverNetworkDelay(kOneWayNetworkDelay); |
| SetReceiverToSenderNetworkDelay(kOneWayNetworkDelay); |
| |
| // Expect that each packet is only sent once. |
| std::set<std::pair<FrameId, FramePacketId>> received_packets; |
| EXPECT_CALL(*receiver(), OnRtpPacket(_)) |
| .WillRepeatedly([&](const RtpPacketParser::ParseResult& parsed_packet) { |
| std::pair<FrameId, FramePacketId> id(parsed_packet.frame_id, |
| parsed_packet.packet_id); |
| const auto insert_result = received_packets.insert(id); |
| EXPECT_TRUE(insert_result.second) |
| << "Received duplicate packet: " << id.first << ':' |
| << static_cast<int>(id.second); |
| }); |
| |
| StrictMock<MockObserver> observer; |
| |
| // The sender should be in a valid state during frame cancellations. Since |
| // these all came from the same report, the sender shouldn't have any frames |
| // in flight. |
| EXPECT_CALL(observer, OnFrameCanceled(_)) |
| .Times(3) |
| .WillRepeatedly([sender = sender()](FrameId id) { |
| EXPECT_EQ(0u, sender->GetInFlightFrameCount()); |
| |
| // Since no frames are in flight, the next frame timestamp should not |
| // matter. |
| EXPECT_EQ(Clock::duration::zero(), |
| sender->GetInFlightMediaDuration(RtpTimeTicks(123456789))); |
| }); |
| |
| // Don't ACK frames and return a report until the third frame. |
| EXPECT_CALL(*receiver(), OnFrameComplete(_)).Times(2); |
| EXPECT_CALL(*receiver(), OnFrameComplete(FrameId::first() + 2)) |
| .WillOnce(InvokeWithoutArgs([&] { |
| if (receiver()->AutoAdvanceCheckpoint()) { |
| receiver()->TransmitRtcpFeedbackPacket(); |
| } |
| })); |
| |
| sender()->SetObserver(&observer); |
| |
| EncodedFrameWithBuffer frames[3]; |
| constexpr int kFrameDataSizes[] = {8196, 12, 1900}; |
| for (int i = 0; i < 3; ++i) { |
| EXPECT_EQ(i == 0, sender()->NeedsKeyFrame()); |
| PopulateFrameWithDefaults(FrameId::first() + i, |
| FakeClock::now() - kCaptureDelay, 0xbf - i, |
| kFrameDataSizes[i], &frames[i]); |
| |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(frames[i])); |
| SimulateExecution(kFrameDuration); |
| } |
| SimulateExecution(kTargetPlayoutDelay); |
| |
| ExpectFramesReceivedCorrectly(frames, receiver()->TakeCompleteFrames()); |
| } |
| |
| // Tests that the Sender correctly computes the current in-flight media |
| // duration, a backlog signal for clients. |
| TEST_F(SenderTest, ComputesInFlightMediaDuration) { |
| // With no frames enqueued, the in-flight media duration should be zero. |
| EXPECT_EQ(Clock::duration::zero(), |
| sender()->GetInFlightMediaDuration(RtpTimeTicks())); |
| EXPECT_EQ(Clock::duration::zero(), |
| sender()->GetInFlightMediaDuration( |
| RtpTimeTicks() + RtpTimeDelta::FromTicks(kRtpTicksPerFrame))); |
| |
| // Enqueue a frame. |
| EncodedFrameWithBuffer frame; |
| PopulateFrameWithDefaults(FrameId::first(), FakeClock::now(), 0, |
| 13 /* bytes */, &frame); |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(frame)); |
| |
| // Now, the in-flight media duration should depend on the RTP timestamp of the |
| // next frame. |
| EXPECT_EQ(kFrameDuration, sender()->GetInFlightMediaDuration( |
| frame.rtp_timestamp + |
| RtpTimeDelta::FromTicks(kRtpTicksPerFrame))); |
| EXPECT_EQ(10 * kFrameDuration, |
| sender()->GetInFlightMediaDuration( |
| frame.rtp_timestamp + |
| RtpTimeDelta::FromTicks(10 * kRtpTicksPerFrame))); |
| } |
| |
| // Tests that the Sender computes the maximum in-flight media duration based on |
| // its analysis of current network conditions. By implication, this demonstrates |
| // that the Sender is also measuring the network round-trip time. |
| TEST_F(SenderTest, RespondsToNetworkLatencyChanges) { |
| // The expected maximum error in time calculations is one tick of the RTCP |
| // report block's delay type. |
| constexpr auto kEpsilon = to_nanoseconds(RtcpReportBlock::Delay(1)); |
| |
| // Before the Sender has measured the network round-trip time, the in-flight |
| // limit is pinned to the minimum. |
| EXPECT_EQ(Clock::duration::zero(), sender()->GetCurrentRoundTripTime()); |
| EXPECT_EQ(kMinSenderInFlight, sender()->GetMaxInFlightMediaDuration()); |
| |
| // Enqueue a frame in the Sender to start emitting periodic RTCP reports. |
| { |
| EncodedFrameWithBuffer frame; |
| PopulateFrameWithDefaults(FrameId::first(), FakeClock::now(), 0, |
| 1 /* byte */, &frame); |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(frame)); |
| } |
| |
| // Simulate one-way network delays large enough that twice the round-trip time |
| // exceeds the floor, so the in-flight limit tracks the measured RTT. |
| constexpr milliseconds kOutboundDelay(20); |
| constexpr milliseconds kInboundDelay(20); |
| constexpr milliseconds kRoundTripDelay(kOutboundDelay + kInboundDelay); |
| SimulateNetworkRoundTrip(kOutboundDelay, kInboundDelay); |
| |
| // At this point, the Sender should have measured the network round-trip time, |
| // and the in-flight limit should track exactly twice that value. |
| EXPECT_NEARLY_EQUAL(kRoundTripDelay, sender()->GetCurrentRoundTripTime(), |
| kEpsilon); |
| EXPECT_EQ(2 * sender()->GetCurrentRoundTripTime(), |
| sender()->GetMaxInFlightMediaDuration()); |
| |
| // Increase the outbound delay by a small amount, which will increase the |
| // total round-trip time. The delta is kept small so the smoothing filter |
| // converges to within kEpsilon over kNumReportIntervals, and so that twice |
| // the round-trip time stays within the in-flight clamp bounds. |
| constexpr milliseconds kIncreasedOutboundDelay(24); |
| constexpr milliseconds kIncreasedRoundTripDelay(kIncreasedOutboundDelay + |
| kInboundDelay); |
| SetSenderToReceiverNetworkDelay(kIncreasedOutboundDelay); |
| |
| // With increased network delay, run several more network round-trips. Expect |
| // the Sender to gradually converge towards the new network round-trip time. |
| constexpr int kNumReportIntervals = 50; |
| EXPECT_CALL(*receiver(), OnSenderReport(_)) |
| .Times(AtLeast(kNumReportIntervals - 1)) |
| .WillRepeatedly( |
| [&](const SenderReportParser::SenderReportWithId& sender_report) { |
| receiver()->SetReceiverReport(sender_report.report_id, |
| RtcpReportBlock::Delay::zero()); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| }); |
| Clock::duration last_max = sender()->GetMaxInFlightMediaDuration(); |
| for (int i = 0; i < kNumReportIntervals; ++i) { |
| SimulateExecution(kRtcpReportInterval); |
| const Clock::duration updated_value = |
| sender()->GetMaxInFlightMediaDuration(); |
| EXPECT_LE(last_max, updated_value); |
| last_max = updated_value; |
| } |
| EXPECT_NEARLY_EQUAL(kIncreasedRoundTripDelay, |
| sender()->GetCurrentRoundTripTime(), kEpsilon); |
| EXPECT_EQ(2 * sender()->GetCurrentRoundTripTime(), |
| sender()->GetMaxInFlightMediaDuration()); |
| } |
| |
| // Tests that the in-flight media duration is capped at a third of the playout |
| // delay under high latency, so the Sender backs off rather than over-buffering |
| // and reserves the majority of the playout window for the Receiver. |
| TEST_F(SenderTest, CapsInFlightMediaDurationUnderHighLatency) { |
| // Enqueue a frame so the Sender begins emitting periodic RTCP reports. |
| { |
| EncodedFrameWithBuffer frame; |
| PopulateFrameWithDefaults(FrameId::first(), FakeClock::now(), 0, |
| 1 /* byte */, &frame); |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(frame)); |
| } |
| |
| // A large round-trip time, so that twice the RTT exceeds a third of the |
| // playout delay and the upper bound takes effect. |
| constexpr milliseconds kOutboundDelay(50); |
| constexpr milliseconds kInboundDelay(50); |
| SimulateNetworkRoundTrip(kOutboundDelay, kInboundDelay); |
| |
| const Clock::duration cap = Clock::to_duration(kTargetPlayoutDelay) / 3; |
| ASSERT_GT(2 * sender()->GetCurrentRoundTripTime(), cap); |
| EXPECT_EQ(cap, sender()->GetMaxInFlightMediaDuration()); |
| } |
| |
| // Tests that when a third of the playout delay falls below the in-flight floor, |
| // the floor wins and the std::clamp() bounds remain well-ordered (no |
| // precondition violation). |
| TEST_F(SenderTest, EnforcesMinimumInFlightForSmallPlayoutDelays) { |
| // A third of this playout delay (30ms) is below the 66ms floor, so the floor |
| // wins and the std::clamp() bounds remain well-ordered. |
| EXPECT_EQ(kMinSenderInFlight, MaxInFlightForPlayoutDelay(milliseconds(90))); |
| } |
| |
| // Tests the asymmetric "fast attack, slow decay" round-trip-time smoothing |
| // filter directly. See crbug.com/498036656. |
| TEST(SenderRoundTripTimeSmoothingTest, ReactsQuicklyToSpikesAndDecaysSlowly) { |
| // A zero estimate adopts the first measurement directly. |
| EXPECT_EQ(Clock::to_duration(milliseconds(20)), |
| SenderImpl::SmoothRoundTripTime(Clock::duration::zero(), |
| milliseconds(20))); |
| |
| // Fast attack: an upward spike pulls the estimate halfway to the measurement |
| // in a single step: (20 + 120) / 2 == 70ms. |
| const Clock::duration baseline = milliseconds(20); |
| const Clock::duration after_spike = |
| SenderImpl::SmoothRoundTripTime(baseline, milliseconds(120)); |
| EXPECT_EQ(Clock::to_duration(milliseconds(70)), after_spike); |
| |
| // Slow decay: a downward measurement only moves the estimate an eighth of the |
| // way: (7 * 70 + 20) / 8 == 63.75ms. |
| const Clock::duration after_recovery = |
| SenderImpl::SmoothRoundTripTime(after_spike, milliseconds(20)); |
| EXPECT_EQ((7 * after_spike + Clock::to_duration(milliseconds(20))) / 8, |
| after_recovery); |
| |
| // The upward reaction is far larger than the downward reaction. |
| EXPECT_GT(after_spike - baseline, after_spike - after_recovery); |
| |
| // An equal measurement leaves the estimate unchanged. |
| EXPECT_EQ(baseline, SenderImpl::SmoothRoundTripTime(baseline, baseline)); |
| } |
| |
| // Tests that the Sender rejects frames if too large a span of FrameIds would be |
| // in-flight at once. |
| TEST_F(SenderTest, RejectsEnqueuingBeforeProtocolDesignLimit) { |
| // For this test, use a high frame rate so that all kMaxUnackedFrames frames |
| // fit within the in-flight media-duration limit, isolating the FrameId-span |
| // rejection logic (rather than triggering media-duration rejection first). |
| constexpr int kFramesPerSecond = 4000; |
| constexpr milliseconds kSmallFrameDuration(1); |
| |
| // Send the absolute design-limit maximum number of frames. |
| int frame_count = 0; |
| for (; frame_count < kMaxUnackedFrames; ++frame_count) { |
| EncodedFrameWithBuffer frame; |
| PopulateFrameWithDefaults(sender()->GetNextFrameId(), FakeClock::now(), 0, |
| 13 /* bytes */, &frame); |
| OverrideRtpTimestamp(frame_count, &frame, kFramesPerSecond); |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(frame)); |
| SimulateExecution(kSmallFrameDuration); |
| } |
| |
| // Now, attempting to enqueue just one more frame should fail. |
| EncodedFrameWithBuffer one_frame_too_much; |
| PopulateFrameWithDefaults(sender()->GetNextFrameId(), FakeClock::now(), 0, |
| 13 /* bytes */, &one_frame_too_much); |
| OverrideRtpTimestamp(frame_count++, &one_frame_too_much, kFramesPerSecond); |
| EXPECT_EQ(Sender::REACHED_ID_SPAN_LIMIT, |
| sender()->EnqueueFrame(one_frame_too_much)); |
| SimulateExecution(kSmallFrameDuration); |
| |
| // Now, simulate the Receiver ACKing the first frame, and enqueuing should |
| // then succeed again. |
| receiver()->SetCheckpointFrame(FrameId::first()); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| SimulateExecution(); // RTCP transmitted to Sender. |
| EXPECT_EQ(Sender::OK, sender()->EnqueueFrame(one_frame_too_much)); |
| SimulateExecution(kSmallFrameDuration); |
| |
| // Finally, attempting to enqueue another frame should fail again. |
| EncodedFrameWithBuffer another_frame_too_much; |
| PopulateFrameWithDefaults(sender()->GetNextFrameId(), FakeClock::now(), 0, |
| 13 /* bytes */, &another_frame_too_much); |
| OverrideRtpTimestamp(frame_count++, &another_frame_too_much, |
| kFramesPerSecond); |
| EXPECT_EQ(Sender::REACHED_ID_SPAN_LIMIT, |
| sender()->EnqueueFrame(another_frame_too_much)); |
| SimulateExecution(kSmallFrameDuration); |
| } |
| |
| TEST_F(SenderTest, CanCancelAllInFlightFrames) { |
| StrictMock<MockObserver> observer; |
| sender()->SetObserver(&observer); |
| |
| // Send the absolute design-limit maximum number of frames. |
| for (int i = 0; i < kMaxUnackedFrames; ++i) { |
| EncodedFrameWithBuffer frame; |
| PopulateFrameWithDefaults(sender()->GetNextFrameId(), FakeClock::now(), 0, |
| 13 /* bytes */, &frame); |
| // High frame rate so all kMaxUnackedFrames frames fit within the in-flight |
| // media-duration limit. |
| OverrideRtpTimestamp(i, &frame, 4000 /* fps */); |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(frame)); |
| SimulateExecution(kFrameDuration); |
| } |
| |
| EXPECT_CALL(observer, OnFrameCanceled(_)).Times(kMaxUnackedFrames); |
| sender()->CancelInFlightData(); |
| } |
| |
| // Tests that the Sender rejects frames if too-long a media duration is |
| // in-flight. This is the Sender's primary flow control mechanism. |
| TEST_F(SenderTest, RejectsEnqueuingIfTooLongMediaDurationIsInFlight) { |
| // For this test, use 20 FPS. This makes all frames 50 ms apart, which should |
| // make it easy to trigger the media-duration rejection logic. |
| constexpr int kFramesPerSecond = 20; |
| constexpr milliseconds kLargeFrameDuration(50); |
| |
| // Enqueue frames until one is rejected because the in-flight duration would |
| // be too high. |
| EncodedFrameWithBuffer frame; |
| int frame_count = 0; |
| for (; frame_count < kMaxUnackedFrames; ++frame_count) { |
| PopulateFrameWithDefaults(sender()->GetNextFrameId(), FakeClock::now(), 0, |
| 13 /* bytes */, &frame); |
| OverrideRtpTimestamp(frame_count, &frame, kFramesPerSecond); |
| const auto result = sender()->EnqueueFrame(frame); |
| SimulateExecution(kLargeFrameDuration); |
| if (result == Sender::MAX_DURATION_IN_FLIGHT) { |
| break; |
| } |
| ASSERT_EQ(Sender::OK, result); |
| } |
| |
| // Now, simulate the Receiver ACKing the first frame, and enqueuing should |
| // then succeed again. |
| receiver()->SetCheckpointFrame(FrameId::first()); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| SimulateExecution(); // RTCP transmitted to Sender. |
| EXPECT_EQ(Sender::OK, sender()->EnqueueFrame(frame)); |
| SimulateExecution(kLargeFrameDuration); |
| |
| // However, attempting to enqueue another frame should fail again. |
| EncodedFrameWithBuffer one_frame_too_much; |
| PopulateFrameWithDefaults(sender()->GetNextFrameId(), FakeClock::now(), 0, |
| 13 /* bytes */, &one_frame_too_much); |
| OverrideRtpTimestamp(++frame_count, &one_frame_too_much, kFramesPerSecond); |
| EXPECT_EQ(Sender::MAX_DURATION_IN_FLIGHT, |
| sender()->EnqueueFrame(one_frame_too_much)); |
| SimulateExecution(kLargeFrameDuration); |
| } |
| |
| // Tests that the Sender correctly dispatches frame drop events to the |
| // statistics collector. |
| TEST_F(SenderTest, ReportFrameDropEvent) { |
| StrictMock<MockObserver> observer; |
| sender()->SetObserver(&observer); |
| |
| const FrameId frame_id = FrameId::first(); |
| const RtpTimeTicks rtp_timestamp(12345); |
| const Clock::time_point drop_time = FakeClock::now() + milliseconds(10); |
| |
| sender()->ReportFrameDropEvent(frame_id, rtp_timestamp, drop_time); |
| |
| // Verification relies on the underlying StatisticsCollector and Dispatcher |
| // being properly hooked up, which is tested in their respective unit tests. |
| // In `SenderTest`, we primarily just make sure calling this does not crash. |
| // We can also verify it through the `TakeRecentFrameEvents()` on a mock/fake |
| // if `StatisticsCollector` is accessible, but `SenderTest` environment uses |
| // the real one. |
| } |
| |
| // Tests that the Sender propagates the Receiver's picture loss indicator to the |
| // Observer::OnPictureLost(), and via calls to NeedsKeyFrame(); but only when |
| // producing a key frame is absolutely necessary. |
| TEST_F(SenderTest, ManagesReceiverPictureLossWorkflow) { |
| StrictMock<MockObserver> observer; |
| sender()->SetObserver(&observer); |
| |
| // Send three frames... |
| EncodedFrameWithBuffer frames[6]; |
| for (int i = 0; i < 3; ++i) { |
| if (i == 0) { |
| EXPECT_TRUE(sender()->NeedsKeyFrame()); |
| } else { |
| EXPECT_FALSE(sender()->NeedsKeyFrame()); |
| } |
| PopulateFrameWithDefaults(FrameId::first() + i, |
| FakeClock::now() - kCaptureDelay, 0, |
| 24 /* bytes */, &frames[i]); |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(frames[i])); |
| SimulateExecution(kFrameDuration); |
| } |
| SimulateExecution(kTargetPlayoutDelay); |
| |
| // Simulate the Receiver ACK'ing the first three frames. |
| EXPECT_CALL(observer, OnFrameCanceled(FrameId::first())); |
| EXPECT_CALL(observer, OnFrameCanceled(FrameId::first() + 1)); |
| EXPECT_CALL(observer, OnFrameCanceled(FrameId::first() + 2)); |
| EXPECT_CALL(observer, OnPictureLost()).Times(0); |
| receiver()->SetCheckpointFrame(frames[2].frame_id); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| SimulateExecution(); // RTCP transmitted to Sender. |
| Mock::VerifyAndClearExpectations(&observer); |
| |
| // Simulate something going wrong in the Receiver, and have it report picture |
| // loss to the Sender. The Sender should then propagate this to its Observer |
| // and return true when NeedsKeyFrame() is called. |
| EXPECT_CALL(observer, OnFrameCanceled(_)).Times(0); |
| EXPECT_CALL(observer, OnPictureLost()); |
| EXPECT_FALSE(sender()->NeedsKeyFrame()); |
| receiver()->SetPictureLossIndicator(true); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| SimulateExecution(); // RTCP transmitted to Sender. |
| Mock::VerifyAndClearExpectations(&observer); |
| EXPECT_TRUE(sender()->NeedsKeyFrame()); |
| |
| // Send a non-key frame, and expect NeedsKeyFrame() still returns true. The |
| // Observer is not re-notified. This accounts for the case where a client's |
| // media encoder had frames in its processing pipeline before NeedsKeyFrame() |
| // began returning true. |
| EXPECT_CALL(observer, OnFrameCanceled(_)).Times(0); |
| EXPECT_CALL(observer, OnPictureLost()).Times(0); |
| EncodedFrameWithBuffer& nonkey_frame = frames[3]; |
| PopulateFrameWithDefaults(FrameId::first() + 3, |
| FakeClock::now() - kCaptureDelay, 0, 24 /* bytes */, |
| &nonkey_frame); |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(nonkey_frame)); |
| SimulateExecution(kFrameDuration); |
| Mock::VerifyAndClearExpectations(&observer); |
| EXPECT_TRUE(sender()->NeedsKeyFrame()); |
| |
| // Now send a key frame, and expect NeedsKeyFrame() returns false. Note that |
| // the Receiver hasn't cleared the PLI condition, but the Sender knows more |
| // key frames won't be needed. |
| EXPECT_CALL(observer, OnFrameCanceled(_)).Times(0); |
| EXPECT_CALL(observer, OnPictureLost()).Times(0); |
| EncodedFrameWithBuffer& recovery_frame = frames[4]; |
| PopulateFrameWithDefaults(FrameId::first() + 4, |
| FakeClock::now() - kCaptureDelay, 0, 24 /* bytes */, |
| &recovery_frame); |
| recovery_frame.dependency = EncodedFrame::Dependency::kKeyFrame; |
| recovery_frame.referenced_frame_id = recovery_frame.frame_id; |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(recovery_frame)); |
| SimulateExecution(kFrameDuration); |
| Mock::VerifyAndClearExpectations(&observer); |
| EXPECT_FALSE(sender()->NeedsKeyFrame()); |
| |
| // Let's say the Receiver hasn't received the key frame yet, and it reports |
| // its picture loss again to the Sender. Observer::OnPictureLost() should not |
| // be called, and NeedsKeyFrame() should NOT return true, because the Sender |
| // knows the Receiver hasn't acknowledged the key frame (just sent) yet. |
| EXPECT_CALL(observer, OnFrameCanceled(nonkey_frame.frame_id)); |
| EXPECT_CALL(observer, OnPictureLost()).Times(0); |
| receiver()->SetCheckpointFrame(nonkey_frame.frame_id); |
| receiver()->SetPictureLossIndicator(true); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| SimulateExecution(); // RTCP transmitted to Sender. |
| Mock::VerifyAndClearExpectations(&observer); |
| EXPECT_FALSE(sender()->NeedsKeyFrame()); |
| |
| // Now, simulate the Receiver getting the key frame, but NOT recovering. This |
| // should cause Observer::OnPictureLost() to be called, and cause |
| // NeedsKeyFrame() to return true again. |
| EXPECT_CALL(observer, OnFrameCanceled(recovery_frame.frame_id)); |
| EXPECT_CALL(observer, OnPictureLost()); |
| receiver()->SetCheckpointFrame(recovery_frame.frame_id); |
| receiver()->SetPictureLossIndicator(true); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| SimulateExecution(); // RTCP transmitted to Sender. |
| Mock::VerifyAndClearExpectations(&observer); |
| EXPECT_TRUE(sender()->NeedsKeyFrame()); |
| |
| // Send another key frame, and expect NeedsKeyFrame() returns false. |
| EXPECT_CALL(observer, OnFrameCanceled(_)).Times(0); |
| EXPECT_CALL(observer, OnPictureLost()).Times(0); |
| EncodedFrameWithBuffer& another_recovery_frame = frames[5]; |
| PopulateFrameWithDefaults(FrameId::first() + 5, |
| FakeClock::now() - kCaptureDelay, 0, 24 /* bytes */, |
| &another_recovery_frame); |
| another_recovery_frame.dependency = EncodedFrame::Dependency::kKeyFrame; |
| another_recovery_frame.referenced_frame_id = another_recovery_frame.frame_id; |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(another_recovery_frame)); |
| SimulateExecution(kFrameDuration); |
| Mock::VerifyAndClearExpectations(&observer); |
| EXPECT_FALSE(sender()->NeedsKeyFrame()); |
| |
| // Now, simulate the Receiver recovering. It will report this to the Sender, |
| // and NeedsKeyFrame() will still return false. |
| EXPECT_CALL(observer, OnFrameCanceled(another_recovery_frame.frame_id)); |
| EXPECT_CALL(observer, OnPictureLost()).Times(0); |
| receiver()->SetCheckpointFrame(another_recovery_frame.frame_id); |
| receiver()->SetPictureLossIndicator(false); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| SimulateExecution(); // RTCP transmitted to Sender. |
| Mock::VerifyAndClearExpectations(&observer); |
| EXPECT_FALSE(sender()->NeedsKeyFrame()); |
| |
| ExpectFramesReceivedCorrectly(frames, receiver()->TakeCompleteFrames()); |
| } |
| |
| // Tests that the Receiver should get a Sender Report just before the first RTP |
| // packet, and at regular intervals thereafter. The Sender Report contains the |
| // lip-sync information necessary for play-out timing. |
| TEST_F(SenderTest, ProvidesSenderReports) { |
| std::vector<SenderReportParser::SenderReportWithId> sender_reports; |
| Sequence packet_sequence; |
| EXPECT_CALL(*receiver(), OnSenderReport(_)) |
| .InSequence(packet_sequence) |
| .WillOnce([&](const SenderReportParser::SenderReportWithId& report) { |
| sender_reports.push_back(report); |
| }) |
| .RetiresOnSaturation(); |
| EXPECT_CALL(*receiver(), OnRtpPacket(_)).InSequence(packet_sequence); |
| EXPECT_CALL(*receiver(), OnSenderReport(_)) |
| .Times(3) |
| .InSequence(packet_sequence) |
| .WillRepeatedly( |
| [&](const SenderReportParser::SenderReportWithId& report) { |
| sender_reports.push_back(report); |
| }); |
| |
| EncodedFrameWithBuffer frame; |
| constexpr int kFrameDataSize = 250; |
| PopulateFrameWithDefaults(FrameId::first(), FakeClock::now(), 0, |
| kFrameDataSize, &frame); |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(frame)); |
| SimulateExecution(); // Should send one Sender Report + one RTP packet. |
| EXPECT_EQ(size_t{1}, sender_reports.size()); |
| |
| // Have the Receiver ACK the frame to prevent retransmitting the RTP packet. |
| receiver()->SetCheckpointFrame(FrameId::first()); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| SimulateExecution(); // RTCP transmitted to Sender. |
| |
| // Advance through three more reporting intervals. One Sender Report should be |
| // sent each interval, making a total of 4 reports sent. |
| constexpr auto kThreeReportIntervals = 3 * kRtcpReportInterval; |
| SimulateExecution(kThreeReportIntervals); // Three more Sender Reports. |
| ASSERT_EQ(size_t{4}, sender_reports.size()); |
| |
| // The first report should contain the same timestamps as the frame because |
| // the Clock did not advance. Also, its packet count and octet count fields |
| // should be zero since the report was sent before the RTP packet. |
| EXPECT_EQ(frame.reference_time, sender_reports.front().reference_time); |
| EXPECT_EQ(frame.rtp_timestamp, sender_reports.front().rtp_timestamp); |
| EXPECT_EQ(uint32_t{0}, sender_reports.front().send_packet_count); |
| EXPECT_EQ(uint32_t{0}, sender_reports.front().send_octet_count); |
| |
| // The last report should contain the timestamps extrapolated into the future |
| // because the Clock did move forward. Also, the packet count and octet fields |
| // should now be non-zero because the report was sent after the RTP packet. |
| EXPECT_EQ(frame.reference_time + kThreeReportIntervals, |
| sender_reports.back().reference_time); |
| EXPECT_EQ(frame.rtp_timestamp + |
| RtpTimeDelta::FromDuration(kThreeReportIntervals, kRtpTimebase), |
| sender_reports.back().rtp_timestamp); |
| EXPECT_EQ(uint32_t{1}, sender_reports.back().send_packet_count); |
| EXPECT_EQ(uint32_t{kFrameDataSize}, sender_reports.back().send_octet_count); |
| } |
| |
| TEST_F(SenderTest, ReferenceTimesCanBeNonMonotonic) { |
| // This tests that the sender is robust to encoded frames with non-monotonic |
| // reference times. This situation does not prevent frames from being |
| // transmitted in correct order; however, lip sync will suffer if reference |
| // times do not correspond to RTP timestamps. |
| EXPECT_CALL(*receiver(), OnRtpPacket(_)).Times(AtLeast(1)); |
| EXPECT_CALL(*receiver(), OnSenderReport(_)).Times(AtLeast(1)); |
| |
| // Send the 10 frames with non-monotonic reference times. |
| Clock::time_point reference_time = FakeClock::now(); |
| for (int i = 0; i < 10; ++i) { |
| EncodedFrameWithBuffer frame; |
| PopulateFrameWithDefaults(sender()->GetNextFrameId(), reference_time, 0, |
| 13 /* bytes */, &frame); |
| // High frame rate so all frames fit within the in-flight media-duration |
| // limit; this test exercises non-monotonic reference times, not RTP span. |
| OverrideRtpTimestamp(i, &frame, 1000 /* fps */); |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(frame)); |
| SimulateExecution(kFrameDuration); |
| reference_time -= microseconds(10); |
| } |
| } |
| |
| // Tests that the Sender provides Kickstart packets whenever the Receiver may |
| // not know about new frames. |
| TEST_F(SenderTest, ProvidesKickstartPacketsIfReceiverDoesNotACK) { |
| // Have the Receiver move the checkpoint forward only for the first frame, and |
| // none of the later frames. This will force the Sender to eventually send a |
| // Kickstart packet. |
| ON_CALL(*receiver(), OnFrameComplete(_)).WillByDefault([&](FrameId frame_id) { |
| if (frame_id == FrameId::first()) { |
| receiver()->SetCheckpointFrame(FrameId::first()); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| } |
| }); |
| |
| // Send three frames, paced to the media. |
| EncodedFrameWithBuffer frames[3]; |
| for (int i = 0; i < 3; ++i) { |
| PopulateFrameWithDefaults(FrameId::first() + i, |
| FakeClock::now() - kCaptureDelay, i, |
| 48 /* bytes */, &frames[i]); |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(frames[i])); |
| SimulateExecution(kFrameDuration); |
| } |
| |
| // Now, do nothing for a while. Because the Receiver isn't moving the |
| // checkpoint forward, the Sender will have sent all the RTP packets at least |
| // once, and then will start sending just Kickstart packets. |
| SimulateExecution(kTargetPlayoutDelay); |
| |
| // Keep doing nothing for a while, and confirm the Sender is just sending the |
| // same Kickstart packet over and over. The Kickstart packet is supposed to be |
| // the last packet of the latest frame. |
| std::set<std::pair<FrameId, FramePacketId>> unique_received_packet_ids; |
| EXPECT_CALL(*receiver(), OnRtpPacket(_)) |
| .WillRepeatedly([&](const RtpPacketParser::ParseResult& parsed_packet) { |
| unique_received_packet_ids.emplace(parsed_packet.frame_id, |
| parsed_packet.packet_id); |
| }); |
| SimulateExecution(kTargetPlayoutDelay); |
| Mock::VerifyAndClearExpectations(receiver()); |
| EXPECT_EQ(size_t{1}, unique_received_packet_ids.size()); |
| EXPECT_EQ(frames[2].frame_id, unique_received_packet_ids.begin()->first); |
| |
| // Now, simulate the Receiver ACKing all the frames. |
| receiver()->SetCheckpointFrame(frames[2].frame_id); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| SimulateExecution(); // RTCP transmitted to Sender. |
| |
| // With all the frames sent, the Sender should not be transmitting anything. |
| EXPECT_CALL(*receiver(), OnRtpPacket(_)).Times(0); |
| SimulateExecution(10 * kTargetPlayoutDelay); |
| |
| ExpectFramesReceivedCorrectly(frames, receiver()->TakeCompleteFrames()); |
| } |
| |
| // Tests that the Sender only retransmits packets specifically NACK'ed by the |
| // Receiver. |
| TEST_F(SenderTest, ResendsIndividuallyNackedPackets) { |
| // Populate the frame data in each frame with enough bytes to force at least |
| // three RTP packets per frame. |
| constexpr int kFrameDataSize = 3 * kMaxRtpPacketSizeForIpv6UdpOnEthernet; |
| |
| // Use a 1ms network delay in each direction to make the sequence of events |
| // clearer in this test. |
| constexpr milliseconds kOneWayNetworkDelay(1); |
| SetSenderToReceiverNetworkDelay(kOneWayNetworkDelay); |
| SetReceiverToSenderNetworkDelay(kOneWayNetworkDelay); |
| |
| // Simulate that three specific packets will be dropped by the network, one |
| // from each frame (about to be sent). |
| const std::vector<PacketNack> dropped_packets{ |
| {FrameId::first(), FramePacketId{2}}, |
| {FrameId::first() + 1, FramePacketId{1}}, |
| {FrameId::first() + 2, FramePacketId{0}}, |
| }; |
| receiver()->SetIgnoreList(dropped_packets); |
| |
| // Send three frames, paced to the media. The Receiver won't completely |
| // receive any of these frames due to dropped packets. |
| EXPECT_CALL(*receiver(), OnFrameComplete(_)).Times(0); |
| EncodedFrameWithBuffer frames[3]; |
| for (int i = 0; i < 3; ++i) { |
| PopulateFrameWithDefaults(FrameId::first() + i, |
| FakeClock::now() - kCaptureDelay, i, |
| kFrameDataSize, &frames[i]); |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(frames[i])); |
| SimulateExecution(kFrameDuration); |
| } |
| SimulateExecution(kTargetPlayoutDelay); |
| Mock::VerifyAndClearExpectations(receiver()); |
| EXPECT_EQ(3u, sender()->GetInFlightFrameCount()); |
| |
| // The Receiver NACKs the three dropped packets... |
| receiver()->SetNacksAndAcks(dropped_packets, {}); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| |
| // In the meantime, the network recovers (i.e., no more dropped packets)... |
| receiver()->SetIgnoreList({}); |
| |
| // The NACKs reach the Sender, and it acts on them by retransmitting. |
| SimulateExecution(kOneWayNetworkDelay); |
| |
| // As each retransmitted packet arrives at the Receiver, advance the |
| // checkpoint forward to notify the Sender of frames that are now completely |
| // received. Also, confirm that only the three specifically-NACK'ed packets |
| // were retransmitted. |
| EXPECT_CALL(*receiver(), OnFrameComplete(_)) |
| .Times(3) |
| .WillRepeatedly(InvokeWithoutArgs([&] { |
| if (receiver()->AutoAdvanceCheckpoint()) { |
| receiver()->TransmitRtcpFeedbackPacket(); |
| } |
| })); |
| EXPECT_CALL(*receiver(), OnRtpPacket(_)) |
| .Times(3) |
| .WillRepeatedly([&](const RtpPacketParser::ParseResult& packet) { |
| EXPECT_TRUE(Contains(dropped_packets, |
| PacketNack{packet.frame_id, packet.packet_id})); |
| }); |
| SimulateExecution(kOneWayNetworkDelay); |
| Mock::VerifyAndClearExpectations(receiver()); |
| |
| // The Receiver checkpoint feedback(s) travel back to the Sender, and there |
| // should no longer be any frames in-flight. |
| SimulateExecution(kOneWayNetworkDelay); |
| EXPECT_EQ(0u, sender()->GetInFlightFrameCount()); |
| |
| // The Sender should not be transmitting anything from now on since all frames |
| // are known to have been completely received. |
| EXPECT_CALL(*receiver(), OnRtpPacket(_)).Times(0); |
| SimulateExecution(10 * kTargetPlayoutDelay); |
| |
| ExpectFramesReceivedCorrectly(frames, receiver()->TakeCompleteFrames()); |
| } |
| |
| // Tests that the Sender retransmits an entire frame if the Receiver requests it |
| // (i.e., a full frame NACK), but does not retransmit any packets for frames |
| // (before or after) that have been acknowledged. |
| TEST_F(SenderTest, ResendsMissingFrames) { |
| // Populate the frame data in each frame with enough bytes to force at least |
| // three RTP packets per frame. |
| constexpr int kFrameDataSize = 3 * kMaxRtpPacketSizeForIpv6UdpOnEthernet; |
| |
| // Use a 1ms network delay in each direction to make the sequence of events |
| // clearer in this test. |
| constexpr milliseconds kOneWayNetworkDelay(1); |
| SetSenderToReceiverNetworkDelay(kOneWayNetworkDelay); |
| SetReceiverToSenderNetworkDelay(kOneWayNetworkDelay); |
| |
| // Simulate that all of the packets for the second frame will be dropped by |
| // the network, but only the packets for that frame. |
| const std::vector<PacketNack> dropped_packets{ |
| {FrameId::first() + 1, kAllPacketsLost}, |
| }; |
| receiver()->SetIgnoreList(dropped_packets); |
| |
| StrictMock<MockObserver> observer; |
| sender()->SetObserver(&observer); |
| |
| // The expectations below track the story and execute simulated Receiver |
| // responses. The Sender will have three frames enqueued by its client, and |
| // then... |
| // |
| // The first frame is received and the Receiver ACKs it by moving the |
| // checkpoint forward. |
| Sequence completion_sequence; |
| EXPECT_CALL(*receiver(), OnFrameComplete(FrameId::first())) |
| .InSequence(completion_sequence) |
| .WillOnce(InvokeWithoutArgs([&] { |
| receiver()->SetCheckpointFrame(FrameId::first()); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| })); |
| // Since all of the packets for the second frame are being dropped, the third |
| // frame will finish next. The Receiver responds by NACKing the second frame |
| // and ACKing the third frame. The checkpoint does not move forward because |
| // the second frame has not been received yet. |
| // |
| // NETWORK CHANGE: After the third frame is received, stop dropping packets. |
| EXPECT_CALL(*receiver(), OnFrameComplete(FrameId::first() + 2)) |
| .InSequence(completion_sequence) |
| .WillOnce(InvokeWithoutArgs([&] { |
| receiver()->SetNacksAndAcks(dropped_packets, |
| std::vector<FrameId>{FrameId::first() + 2}); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| receiver()->SetIgnoreList({}); |
| })); |
| // Finally, the Sender should respond to the whole-frame NACK by re-sending |
| // all of the packets for the second frame, and so the Receiver should |
| // completely receive the frame. |
| EXPECT_CALL(*receiver(), OnFrameComplete(FrameId::first() + 1)) |
| .InSequence(completion_sequence) |
| .WillOnce(InvokeWithoutArgs([&] { |
| receiver()->SetCheckpointFrame(FrameId::first() + 2); |
| receiver()->TransmitRtcpFeedbackPacket(); |
| })); |
| |
| // From the Sender's perspective, the Receiver will ACK the first frame, then |
| // the third frame, then the second frame. |
| Sequence cancel_sequence; |
| EXPECT_CALL(observer, OnFrameCanceled(FrameId::first())) |
| .InSequence(cancel_sequence); |
| EXPECT_CALL(observer, OnFrameCanceled(FrameId::first() + 2)) |
| .InSequence(cancel_sequence); |
| EXPECT_CALL(observer, OnFrameCanceled(FrameId::first() + 1)) |
| .InSequence(cancel_sequence); |
| |
| // With all the expectations/sequences in-place, let 'er rip! |
| EncodedFrameWithBuffer frames[3]; |
| for (int i = 0; i < 3; ++i) { |
| PopulateFrameWithDefaults(FrameId::first() + i, |
| FakeClock::now() - kCaptureDelay, i, |
| kFrameDataSize, &frames[i]); |
| ASSERT_EQ(Sender::OK, sender()->EnqueueFrame(frames[i])); |
| SimulateExecution(kFrameDuration); |
| } |
| SimulateExecution(kTargetPlayoutDelay); |
| Mock::VerifyAndClearExpectations(receiver()); |
| EXPECT_EQ(0u, sender()->GetInFlightFrameCount()); |
| |
| // The Sender should not be transmitting anything from now on since all frames |
| // are known to have been completely received. |
| EXPECT_CALL(*receiver(), OnRtpPacket(_)).Times(0); |
| SimulateExecution(10 * kTargetPlayoutDelay); |
| |
| ExpectFramesReceivedCorrectly(frames, receiver()->TakeCompleteFrames()); |
| } |
| |
| } // namespace |
| } // namespace openscreen::cast |