| // 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" |
| #include "util/chrono_helpers.h" |
| |
| using testing::_; |
| using testing::Invoke; |
| using testing::Mock; |
| using testing::SaveArg; |
| using testing::StrictMock; |
| |
| namespace openscreen { |
| namespace cast { |
| 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 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 + 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 += 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 + 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 += 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 += 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 cast |
| } // namespace openscreen |