aboutsummaryrefslogtreecommitdiff
path: root/cast/streaming/compound_rtcp_builder_unittest.cc
blob: ab6f8c00eab717fe1da6dfbc5bd335deb9d537a2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
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