mediasoup 3.20.3 → 3.20.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/package.json +2 -2
  2. package/worker/include/RTC/SCTP/association/Association.hpp +16 -2
  3. package/worker/include/RTC/SCTP/association/TransmissionControlBlock.hpp +16 -1
  4. package/worker/include/RTC/SCTP/association/TransmissionControlBlockContextInterface.hpp +19 -2
  5. package/worker/include/RTC/SCTP/public/SctpTypes.hpp +2 -0
  6. package/worker/include/RTC/SCTP/rx/ReassemblyQueue.hpp +7 -0
  7. package/worker/include/RTC/SCTP/tx/RetransmissionErrorCounter.hpp +3 -4
  8. package/worker/meson.build +3 -0
  9. package/worker/mocks/include/RTC/SCTP/association/MockAssociationListener.hpp +68 -7
  10. package/worker/mocks/include/handles/MockBackoffTimerHandle.hpp +10 -4
  11. package/worker/mocks/src/handles/MockBackoffTimerHandle.cpp +7 -0
  12. package/worker/src/RTC/SCTP/association/Association.cpp +52 -12
  13. package/worker/src/RTC/SCTP/association/HeartbeatHandler.cpp +33 -13
  14. package/worker/src/RTC/SCTP/association/StreamResetHandler.cpp +15 -11
  15. package/worker/src/RTC/SCTP/association/TransmissionControlBlock.cpp +34 -11
  16. package/worker/src/RTC/SCTP/rx/ReassemblyQueue.cpp +17 -8
  17. package/worker/src/RTC/Transport.cpp +1 -1
  18. package/worker/test/src/RTC/SCTP/association/TestAssociation.cpp +1755 -0
  19. package/worker/test/src/RTC/SCTP/association/TestPacketSender.cpp +121 -0
  20. package/worker/test/src/RTC/SCTP/association/TestStreamResetHandler.cpp +369 -0
  21. package/worker/test/src/RTC/SCTP/association/TestTransmissionControlBlock.cpp +11 -0
  22. package/worker/test/src/RTC/SCTP/rx/TestReassemblyQueue.cpp +24 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasoup",
3
- "version": "3.20.3",
3
+ "version": "3.20.4",
4
4
  "description": "Cutting Edge WebRTC Video Conferencing",
5
5
  "contributors": [
6
6
  "Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)",
@@ -122,7 +122,7 @@
122
122
  "globals": "^17.6.0",
123
123
  "ini": "^6.0.0",
124
124
  "jest": "^30.4.2",
125
- "knip": "^6.15.0",
125
+ "knip": "^6.16.1",
126
126
  "marked": "^18.0.5",
127
127
  "open-cli": "^9.0.0",
128
128
  "pick-port": "^2.2.0",
@@ -53,7 +53,8 @@ namespace RTC
53
53
  */
54
54
  class Association : public AssociationInterface,
55
55
  public PacketSender::Listener,
56
- public BackoffTimerHandleInterface::Listener
56
+ public BackoffTimerHandleInterface::Listener,
57
+ public TransmissionControlBlockContextInterface::Listener
57
58
  {
58
59
  public:
59
60
  /**
@@ -172,7 +173,13 @@ namespace RTC
172
173
  const SctpOptions& sctpOptions,
173
174
  AssociationListenerInterface* listener,
174
175
  SharedInterface* shared,
175
- bool isDataChannel);
176
+ bool isDataChannel,
177
+ // Whether `MayConnect()` should be called when SCTP data is received.
178
+ // This is always true in production (the association auto-initiates the
179
+ // connection as soon as the transport is ready or data arrives). It's
180
+ // only set to false in tests that need a purely passive peer to mimic
181
+ // dcsctp's asymmetric handshake.
182
+ bool mayConnectOnReceivedSctpData);
176
183
 
177
184
  ~Association() override;
178
185
 
@@ -494,6 +501,10 @@ namespace RTC
494
501
  void OnBackoffTimer(
495
502
  BackoffTimerHandleInterface* backoffTimer, uint64_t& baseTimeoutMs, bool& stop) override;
496
503
 
504
+ /* Pure virtual methods inherited from TransmissionControlBlockContextInterface::Listener. */
505
+ public:
506
+ void OnTransmissionControlBlockTooManyTxErrors() override;
507
+
497
508
  private:
498
509
  // SCTP options given in the constructor.
499
510
  SctpOptions sctpOptions;
@@ -526,6 +537,9 @@ namespace RTC
526
537
  const size_t maxPacketLength;
527
538
  // Whether this is DataChannel based SCTP.
528
539
  bool isDataChannel;
540
+ // Whether `MayConnect()` should be called when SCTP data is received.
541
+ // See the constructor for details.
542
+ bool mayConnectOnReceivedSctpData;
529
543
  };
530
544
  } // namespace SCTP
531
545
  } // namespace RTC
@@ -37,6 +37,7 @@ namespace RTC
37
37
  {
38
38
  public:
39
39
  TransmissionControlBlock(
40
+ TransmissionControlBlockContextInterface::Listener* listener,
40
41
  AssociationListenerDeferrer& associationListenerDeferrer,
41
42
  const SctpOptions& sctpOptions,
42
43
  SharedInterface* shared,
@@ -250,7 +251,20 @@ namespace RTC
250
251
  */
251
252
  bool IncrementTxErrorCounter(std::string_view reason) override
252
253
  {
253
- return this->txErrorCounter.Increment(reason);
254
+ const bool withinLimit = this->txErrorCounter.Increment(reason);
255
+
256
+ if (!withinLimit)
257
+ {
258
+ // NOTE: This closes (and destroys) this TCB synchronously. It's safe to
259
+ // do so from within a timer handler because the handler sets the
260
+ // BackoffTimerHandle `stop` flag and doesn't touch any member
261
+ // afterwards, so the (now destroyed) firing timer won't be accessed.
262
+ this->listener->OnTransmissionControlBlockTooManyTxErrors();
263
+ }
264
+
265
+ // NOTE: `withinLimit` is a local, so this is safe even if `this` was
266
+ // destroyed above.
267
+ return withinLimit;
254
268
  }
255
269
 
256
270
  /**
@@ -288,6 +302,7 @@ namespace RTC
288
302
  BackoffTimerHandleInterface* backoffTimer, uint64_t& baseTimeoutMs, bool& stop) override;
289
303
 
290
304
  private:
305
+ TransmissionControlBlockContextInterface::Listener* listener;
291
306
  AssociationListenerDeferrer& associationListenerDeferrer;
292
307
  const SctpOptions sctpOptions;
293
308
  SharedInterface* shared;
@@ -11,6 +11,23 @@ namespace RTC
11
11
  {
12
12
  class TransmissionControlBlockContextInterface
13
13
  {
14
+ public:
15
+ class Listener
16
+ {
17
+ public:
18
+ virtual ~Listener() = default;
19
+
20
+ public:
21
+ /**
22
+ * Called when the transmission error counter has exceeded its limit and
23
+ * the association must therefore be closed.
24
+ *
25
+ * @remarks
26
+ * - It mirrors dcsctp's `CloseConnectionBecauseOfTooManyTransmissionErrors()`.
27
+ */
28
+ virtual void OnTransmissionControlBlockTooManyTxErrors() = 0;
29
+ };
30
+
14
31
  public:
15
32
  virtual ~TransmissionControlBlockContextInterface() = default;
16
33
 
@@ -44,8 +61,8 @@ namespace RTC
44
61
 
45
62
  /**
46
63
  * Increments the transmission error counter, given a human readable
47
- * reason. Returns `true` if the maximum error count has been reached,
48
- * `false` will be returned.
64
+ * reason. Returns `false` if the maximum error count has been reached,
65
+ * `true` otherwise.
49
66
  */
50
67
  virtual bool IncrementTxErrorCounter(std::string_view reason) = 0;
51
68
 
@@ -331,6 +331,8 @@ namespace RTC
331
331
  {
332
332
  return "ERROR_SHUTTING_DOWN";
333
333
  }
334
+
335
+ NO_DEFAULT_GCC();
334
336
  }
335
337
  }
336
338
 
@@ -178,6 +178,13 @@ namespace RTC
178
178
  ReassemblyStreamsInterface::OnAssembledMessage onAssembledMessage,
179
179
  bool useMessageInterleaving);
180
180
 
181
+ /**
182
+ * Treat Forward-TSN message as payload. size is calculated based on wire
183
+ * size rather than used memory size: 32bit for TSN + 16+16 bits for each
184
+ * skipped stream entry.
185
+ */
186
+ size_t ForwardTsnCost(size_t numStreams);
187
+
181
188
  void AddReassembledMessage(std::span<const Types::UnwrappedTsn> tsns, Message message);
182
189
 
183
190
  void AssertIsConsistent() const;
@@ -18,7 +18,7 @@ namespace RTC
18
18
  class RetransmissionErrorCounter
19
19
  {
20
20
  public:
21
- RetransmissionErrorCounter(const SctpOptions& sctpOptions);
21
+ explicit RetransmissionErrorCounter(const SctpOptions& sctpOptions);
22
22
 
23
23
  ~RetransmissionErrorCounter();
24
24
 
@@ -26,14 +26,13 @@ namespace RTC
26
26
  void Dump(int indentation = 0) const;
27
27
 
28
28
  /**
29
- * Increments the retransmission timer. Returns `true` if the maximum
30
- * error count has been reached, `false` will be returned.
29
+ * Increments the retransmission timer. Returns `false` if the maximum
30
+ * error count has been reached, `true` otherwise.
31
31
  */
32
32
  bool Increment(std::string_view reason);
33
33
 
34
34
  /**
35
35
  * Whether maximum error count has been reached.
36
- * @return [description]
37
36
  */
38
37
  bool IsExhausted() const
39
38
  {
@@ -494,9 +494,12 @@ test_sources = [
494
494
  'test/src/RTC/RTCP/TestPacket.cpp',
495
495
  'test/src/RTC/RTCP/TestXr.cpp',
496
496
  'test/src/RTC/SCTP/sctpCommon.cpp',
497
+ 'test/src/RTC/SCTP/association/TestAssociation.cpp',
497
498
  'test/src/RTC/SCTP/association/TestHeartbeatHandler.cpp',
498
499
  'test/src/RTC/SCTP/association/TestNegotiatedCapabilities.cpp',
500
+ 'test/src/RTC/SCTP/association/TestPacketSender.cpp',
499
501
  'test/src/RTC/SCTP/association/TestStateCookie.cpp',
502
+ 'test/src/RTC/SCTP/association/TestStreamResetHandler.cpp',
500
503
  'test/src/RTC/SCTP/association/TestTransmissionControlBlock.cpp',
501
504
  'test/src/RTC/SCTP/rx/TestDataTracker.cpp',
502
505
  'test/src/RTC/SCTP/rx/TestInterleavedReassemblyStreams.cpp',
@@ -21,7 +21,7 @@ namespace mocks
21
21
  {
22
22
  this->sentPackets.emplace_back(data, data + len);
23
23
 
24
- return true;
24
+ return this->sendDataResult;
25
25
  }
26
26
 
27
27
  void OnAssociationConnecting() override
@@ -75,20 +75,35 @@ namespace mocks
75
75
  this->receivedMessages.emplace_back(std::move(message));
76
76
  }
77
77
 
78
- void OnAssociationStreamsResetPerformed(std::span<const uint16_t> /*outboundStreamIds*/) override
78
+ void OnAssociationStreamsResetPerformed(std::span<const uint16_t> outboundStreamIds) override
79
79
  {
80
- // TODO: Do something here for tests.
80
+ ++this->onStreamsResetPerformedCalls;
81
+
82
+ for (const auto streamId : outboundStreamIds)
83
+ {
84
+ this->streamsResetPerformed.insert(streamId);
85
+ }
81
86
  }
82
87
 
83
88
  void OnAssociationStreamsResetFailed(
84
- std::span<const uint16_t> /*outboundStreamIds*/, std::string_view /*errorMessage*/) override
89
+ std::span<const uint16_t> outboundStreamIds, std::string_view /*errorMessage*/) override
85
90
  {
86
- // TODO: Do something here for tests.
91
+ ++this->onStreamsResetFailedCalls;
92
+
93
+ for (const auto streamId : outboundStreamIds)
94
+ {
95
+ this->streamsResetFailed.insert(streamId);
96
+ }
87
97
  }
88
98
 
89
- void OnAssociationInboundStreamsReset(std::span<const uint16_t> /*inboundStreamIds*/) override
99
+ void OnAssociationInboundStreamsReset(std::span<const uint16_t> inboundStreamIds) override
90
100
  {
91
- // TODO: Do something here for tests.
101
+ ++this->onInboundStreamsResetCalls;
102
+
103
+ for (const auto streamId : inboundStreamIds)
104
+ {
105
+ this->inboundStreamsReset.insert(streamId);
106
+ }
92
107
  }
93
108
 
94
109
  void OnAssociationStreamBufferedAmountLow(uint16_t streamId) override
@@ -208,6 +223,45 @@ namespace mocks
208
223
  return this->onTotalBufferedAmountLowCalls;
209
224
  }
210
225
 
226
+ size_t CountOnStreamsResetPerformedCalls() const
227
+ {
228
+ return this->onStreamsResetPerformedCalls;
229
+ }
230
+
231
+ bool HasStreamsResetPerformedForStreamId(uint16_t streamId) const
232
+ {
233
+ return this->streamsResetPerformed.contains(streamId);
234
+ }
235
+
236
+ size_t CountOnStreamsResetFailedCalls() const
237
+ {
238
+ return this->onStreamsResetFailedCalls;
239
+ }
240
+
241
+ bool HasStreamsResetFailedForStreamId(uint16_t streamId) const
242
+ {
243
+ return this->streamsResetFailed.contains(streamId);
244
+ }
245
+
246
+ size_t CountOnInboundStreamsResetCalls() const
247
+ {
248
+ return this->onInboundStreamsResetCalls;
249
+ }
250
+
251
+ bool HasInboundStreamsResetForStreamId(uint16_t streamId) const
252
+ {
253
+ return this->inboundStreamsReset.contains(streamId);
254
+ }
255
+
256
+ /**
257
+ * Sets the value that `OnAssociationSendData()` will return, allowing
258
+ * tests to simulate a failed send.
259
+ */
260
+ void SetSendDataResult(bool sendDataResult)
261
+ {
262
+ this->sendDataResult = sendDataResult;
263
+ }
264
+
211
265
  bool HasSentPackets() const
212
266
  {
213
267
  return !this->sentPackets.empty();
@@ -293,6 +347,13 @@ namespace mocks
293
347
  std::string erroredErrorMessage;
294
348
  std::map<uint16_t /*streamId*/, size_t /*count*/> onStreamBufferedAmountLowCalls;
295
349
  size_t onTotalBufferedAmountLowCalls{ 0 };
350
+ size_t onStreamsResetPerformedCalls{ 0 };
351
+ std::set<uint16_t /*streamId*/> streamsResetPerformed;
352
+ size_t onStreamsResetFailedCalls{ 0 };
353
+ std::set<uint16_t /*streamId*/> streamsResetFailed;
354
+ size_t onInboundStreamsResetCalls{ 0 };
355
+ std::set<uint16_t /*streamId*/> inboundStreamsReset;
356
+ bool sendDataResult{ true };
296
357
  std::deque<std::vector<uint8_t>> sentPackets;
297
358
  std::deque<::RTC::SCTP::Message> receivedMessages;
298
359
  bool transportReady{ true };
@@ -36,14 +36,20 @@ namespace mocks
36
36
 
37
37
  void Start() override
38
38
  {
39
- this->running = true;
40
- this->expiresAtMs = this->getTimeMs() + ComputeNextTimeoutMs();
39
+ this->running = true;
40
+ // NOTE: Reset the expiration count, just like the real BackoffTimerHandle
41
+ // does, so that the backoff starts over from the base timeout.
42
+ this->expirationCount = 0;
43
+ this->expiresAtMs = this->getTimeMs() + ComputeNextTimeoutMs();
41
44
  }
42
45
 
43
46
  void Stop() override
44
47
  {
45
- this->running = false;
46
- this->expiresAtMs = std::numeric_limits<uint64_t>::max();
48
+ this->running = false;
49
+ // NOTE: Reset the expiration count, just like the real BackoffTimerHandle
50
+ // does.
51
+ this->expirationCount = 0;
52
+ this->expiresAtMs = std::numeric_limits<uint64_t>::max();
47
53
  }
48
54
 
49
55
  /**
@@ -123,5 +123,12 @@ namespace mocks
123
123
  {
124
124
  this->expiresAtMs = this->getTimeMs() + ComputeNextTimeoutMs();
125
125
  }
126
+ // Once the timer is no longer running (e.g. max restarts reached), the real
127
+ // BackoffTimerHandle doesn't restart the underlying timer, so it won't fire
128
+ // again. Mirror that here so a stopped timer doesn't keep expiring.
129
+ else
130
+ {
131
+ this->expiresAtMs = std::numeric_limits<uint64_t>::max();
132
+ }
126
133
  }
127
134
  } // namespace mocks
@@ -40,7 +40,8 @@ namespace RTC
40
40
  const SctpOptions& sctpOptions,
41
41
  AssociationListenerInterface* listener,
42
42
  SharedInterface* shared,
43
- bool isDataChannel)
43
+ bool isDataChannel,
44
+ bool mayConnectOnReceivedSctpData)
44
45
  : sctpOptions(sctpOptions),
45
46
  // Our `listener` member is a `AssociationListenerDeferrer` which takes
46
47
  // `listener` argument as constructor argument.
@@ -78,7 +79,8 @@ namespace RTC
78
79
  .maxBackoffTimeoutMs = sctpOptions.timerMaxBackoffTimeoutMs,
79
80
  .maxRestarts = sctpOptions.maxRetransmissions })),
80
81
  maxPacketLength(Utils::Byte::PadDownTo4Bytes(this->sctpOptions.mtu)),
81
- isDataChannel(isDataChannel)
82
+ isDataChannel(isDataChannel),
83
+ mayConnectOnReceivedSctpData(mayConnectOnReceivedSctpData)
82
84
  {
83
85
  MS_TRACE();
84
86
  }
@@ -542,7 +544,13 @@ namespace RTC
542
544
 
543
545
  // If we are received SCTP data from the remote peer it means that we may
544
546
  // initiate the SCTP association (if not already connected).
545
- MayConnect();
547
+ //
548
+ // NOTE: This is disabled in tests that need a purely passive peer to
549
+ // mimic dcsctp's asymmetric handshake.
550
+ if (this->mayConnectOnReceivedSctpData)
551
+ {
552
+ MayConnect();
553
+ }
546
554
 
547
555
  // NOTE: It's important to create the deferrer here, otherwise it may
548
556
  // happen that MayConnect() ends calling to Connect() so we end with two
@@ -749,6 +757,7 @@ namespace RTC
749
757
  MS_TRACE();
750
758
 
751
759
  this->tcb = std::make_unique<TransmissionControlBlock>(
760
+ this,
752
761
  this->associationListenerDeferrer,
753
762
  this->sctpOptions,
754
763
  this->shared,
@@ -2425,6 +2434,15 @@ namespace RTC
2425
2434
  {
2426
2435
  MS_TRACE();
2427
2436
 
2437
+ const auto maxRestarts = this->t1InitTimer->GetMaxRestarts();
2438
+
2439
+ MS_DEBUG_TAG(
2440
+ sctp,
2441
+ "%s timer has expired [expirations:%zu, maxRestarts:%s]",
2442
+ this->t1InitTimer->GetLabel().c_str(),
2443
+ this->t1InitTimer->GetExpirationCount(),
2444
+ maxRestarts ? std::to_string(maxRestarts.value()).c_str() : "Infinite");
2445
+
2428
2446
  const AssociationListenerDeferrer::ScopedDeferrer deferrer(this->associationListenerDeferrer);
2429
2447
 
2430
2448
  AssertState(State::COOKIE_WAIT);
@@ -2445,6 +2463,15 @@ namespace RTC
2445
2463
  {
2446
2464
  MS_TRACE();
2447
2465
 
2466
+ const auto maxRestarts = this->t1CookieTimer->GetMaxRestarts();
2467
+
2468
+ MS_DEBUG_TAG(
2469
+ sctp,
2470
+ "%s timer has expired [expirations:%zu, maxRestarts:%s]",
2471
+ this->t1CookieTimer->GetLabel().c_str(),
2472
+ this->t1CookieTimer->GetExpirationCount(),
2473
+ maxRestarts ? std::to_string(maxRestarts.value()).c_str() : "Infinite");
2474
+
2448
2475
  const AssociationListenerDeferrer::ScopedDeferrer deferrer(this->associationListenerDeferrer);
2449
2476
 
2450
2477
  AssertState(State::COOKIE_ECHOED);
@@ -2467,6 +2494,15 @@ namespace RTC
2467
2494
  {
2468
2495
  MS_TRACE();
2469
2496
 
2497
+ const auto maxRestarts = this->t2ShutdownTimer->GetMaxRestarts();
2498
+
2499
+ MS_DEBUG_TAG(
2500
+ sctp,
2501
+ "%s timer has expired [expirations:%zu, maxRestarts:%s]",
2502
+ this->t2ShutdownTimer->GetLabel().c_str(),
2503
+ this->t2ShutdownTimer->GetExpirationCount(),
2504
+ maxRestarts ? std::to_string(maxRestarts.value()).c_str() : "Infinite");
2505
+
2470
2506
  AssertState(State::SHUTDOWN_SENT, State::SHUTDOWN_ACK_SENT);
2471
2507
  AssertHasTcb();
2472
2508
 
@@ -2811,15 +2847,6 @@ namespace RTC
2811
2847
  {
2812
2848
  MS_TRACE();
2813
2849
 
2814
- const auto maxRestarts = backoffTimer->GetMaxRestarts();
2815
-
2816
- MS_DEBUG_TAG(
2817
- sctp,
2818
- "%s timer has expired [expìrations:%zu/%s]",
2819
- backoffTimer->GetLabel().c_str(),
2820
- backoffTimer->GetExpirationCount(),
2821
- maxRestarts ? std::to_string(maxRestarts.value()).c_str() : "Infinite");
2822
-
2823
2850
  if (backoffTimer == this->t1InitTimer.get())
2824
2851
  {
2825
2852
  OnT1InitTimer(baseTimeoutMs, stop);
@@ -2833,5 +2860,18 @@ namespace RTC
2833
2860
  OnT2ShutdownTimer(baseTimeoutMs, stop);
2834
2861
  }
2835
2862
  }
2863
+
2864
+ void Association::OnTransmissionControlBlockTooManyTxErrors()
2865
+ {
2866
+ MS_TRACE();
2867
+
2868
+ // NOTE: This is invoked from within a TCB timer handler (t3-rtx, heartbeat
2869
+ // or RE-CONFIG timeout). `InternalClose()` destroys the TCB synchronously,
2870
+ // which is safe because the calling timer handler sets the BackoffTimerHandle
2871
+ // `stop` flag and doesn't touch any member afterwards. `InternalClose()` does
2872
+ // not establish its own deferrer scope, so it relies on the active one set up
2873
+ // by the timer handler.
2874
+ InternalClose(Types::ErrorKind::TOO_MANY_RETRIES, "too many transmission errors");
2875
+ }
2836
2876
  } // namespace SCTP
2837
2877
  } // namespace RTC
@@ -34,7 +34,7 @@ namespace RTC
34
34
  .listener = this,
35
35
  .label = "sctp-heartbeat-interval",
36
36
  .baseTimeoutMs = sctpOptions.initialRtoMs,
37
- .backoffAlgorithm = BackoffTimerHandleInterface::BackoffAlgorithm::EXPONENTIAL,
37
+ .backoffAlgorithm = BackoffTimerHandleInterface::BackoffAlgorithm::FIXED,
38
38
  .maxBackoffTimeoutMs = sctpOptions.timerMaxBackoffTimeoutMs,
39
39
  .maxRestarts = std::nullopt })),
40
40
  timeoutTimer(this->shared->CreateBackoffTimer(
@@ -42,7 +42,7 @@ namespace RTC
42
42
  .listener = this,
43
43
  .label = "sctp-heartbeat-timeout",
44
44
  .baseTimeoutMs = sctpOptions.initialRtoMs,
45
- .backoffAlgorithm = BackoffTimerHandleInterface::BackoffAlgorithm::FIXED,
45
+ .backoffAlgorithm = BackoffTimerHandleInterface::BackoffAlgorithm::EXPONENTIAL,
46
46
  .maxBackoffTimeoutMs = std::nullopt,
47
47
  .maxRestarts = 0 }))
48
48
  {
@@ -180,6 +180,18 @@ namespace RTC
180
180
  {
181
181
  MS_TRACE();
182
182
 
183
+ #if MS_LOG_DEV_LEVEL == 3
184
+ const auto maxRestarts = this->intervalTimer->GetMaxRestarts();
185
+ #endif
186
+
187
+ // NOTE: This timer expires periodically on idle connections (forever), so
188
+ // it's logged at dev level to avoid being noisy.
189
+ MS_DEBUG_DEV(
190
+ "%s timer has expired [expirations:%zu, maxRestarts:%s]",
191
+ this->intervalTimer->GetLabel().c_str(),
192
+ this->intervalTimer->GetExpirationCount(),
193
+ maxRestarts ? std::to_string(maxRestarts.value()).c_str() : "Infinite");
194
+
183
195
  // This is a top-level timer entry point (invoked by libuv outside any other
184
196
  // SCTP API call), so it must establish the deferrer scope itself, just like
185
197
  // Association does in its own timer handlers.
@@ -215,10 +227,19 @@ namespace RTC
215
227
  this->tcbContext->SendPacket(packet.get());
216
228
  }
217
229
 
218
- void HeartbeatHandler::OnTimeoutTimer(uint64_t& /*baseTimeoutMs*/, bool& /*stop*/)
230
+ void HeartbeatHandler::OnTimeoutTimer(uint64_t& /*baseTimeoutMs*/, bool& stop)
219
231
  {
220
232
  MS_TRACE();
221
233
 
234
+ const auto maxRestarts = this->timeoutTimer->GetMaxRestarts();
235
+
236
+ MS_DEBUG_TAG(
237
+ sctp,
238
+ "%s timer has expired [expirations:%zu, maxRestarts:%s]",
239
+ this->timeoutTimer->GetLabel().c_str(),
240
+ this->timeoutTimer->GetExpirationCount(),
241
+ maxRestarts ? std::to_string(maxRestarts.value()).c_str() : "Infinite");
242
+
222
243
  // This is a top-level timer entry point (invoked by libuv outside any other
223
244
  // SCTP API call), so it must establish the deferrer scope itself, just like
224
245
  // Association does in its own timer handlers.
@@ -228,7 +249,15 @@ namespace RTC
228
249
  // the interval timer expires.
229
250
  MS_ASSERT(!this->timeoutTimer->IsRunning(), "timeout timer shouldn't be running");
230
251
 
231
- this->tcbContext->IncrementTxErrorCounter("hearbeat timeout");
252
+ if (!this->tcbContext->IncrementTxErrorCounter("hearbeat timeout"))
253
+ {
254
+ // `IncrementTxErrorCounter()` has closed (and destroyed) the TCB (and
255
+ // hence this HeartbeatHandler and its timers). Signal the firing timer to
256
+ // stop and don't touch any member afterwards.
257
+ stop = true;
258
+
259
+ return;
260
+ }
232
261
  }
233
262
 
234
263
  void HeartbeatHandler::OnBackoffTimer(
@@ -236,15 +265,6 @@ namespace RTC
236
265
  {
237
266
  MS_TRACE();
238
267
 
239
- const auto maxRestarts = backoffTimer->GetMaxRestarts();
240
-
241
- MS_DEBUG_TAG(
242
- sctp,
243
- "%s timer has expired [expìrations:%zu/%s]",
244
- backoffTimer->GetLabel().c_str(),
245
- backoffTimer->GetExpirationCount(),
246
- maxRestarts ? std::to_string(maxRestarts.value()).c_str() : "Infinite");
247
-
248
268
  if (backoffTimer == this->intervalTimer.get())
249
269
  {
250
270
  OnIntervalTimer(baseTimeoutMs, stop);
@@ -465,10 +465,19 @@ namespace RTC
465
465
  }
466
466
  }
467
467
 
468
- void StreamResetHandler::OnReConfigTimer(uint64_t& baseTimeoutMs, bool& /*stop*/)
468
+ void StreamResetHandler::OnReConfigTimer(uint64_t& baseTimeoutMs, bool& stop)
469
469
  {
470
470
  MS_TRACE();
471
471
 
472
+ const auto maxRestarts = this->reConfigTimer->GetMaxRestarts();
473
+
474
+ MS_DEBUG_TAG(
475
+ sctp,
476
+ "%s timer has expired [expirations:%zu, maxRestarts:%s]",
477
+ this->reConfigTimer->GetLabel().c_str(),
478
+ this->reConfigTimer->GetExpirationCount(),
479
+ maxRestarts ? std::to_string(maxRestarts.value()).c_str() : "Infinite");
480
+
472
481
  // This is a top-level timer entry point (invoked by libuv outside any other
473
482
  // SCTP API call), so it must establish the deferrer scope itself, just like
474
483
  // Association does in its own timer handlers.
@@ -486,7 +495,11 @@ namespace RTC
486
495
  // response.
487
496
  else if (!this->tcbContext->IncrementTxErrorCounter("RECONFIG timeout"))
488
497
  {
489
- // Timed out. The connection will close after processing the timers.
498
+ // `IncrementTxErrorCounter()` has closed (and destroyed) the TCB (and
499
+ // hence this StreamResetHandler and its timer). Signal the firing timer
500
+ // to stop and don't touch any member afterwards.
501
+ stop = true;
502
+
490
503
  return;
491
504
  }
492
505
  }
@@ -511,15 +524,6 @@ namespace RTC
511
524
  {
512
525
  MS_TRACE();
513
526
 
514
- const auto maxRestarts = backoffTimer->GetMaxRestarts();
515
-
516
- MS_DEBUG_TAG(
517
- sctp,
518
- "%s timer has expired [expìrations:%zu/%s]",
519
- backoffTimer->GetLabel().c_str(),
520
- backoffTimer->GetExpirationCount(),
521
- maxRestarts ? std::to_string(maxRestarts.value()).c_str() : "Infinite");
522
-
523
527
  if (backoffTimer == this->reConfigTimer.get())
524
528
  {
525
529
  OnReConfigTimer(baseTimeoutMs, stop);