mediasoup 3.24.0 → 3.24.2

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ### NEXT
4
4
 
5
+ ### 3.24.2
6
+
7
+ - Worker: Verify `DataConsumer` subchannels before cloning the message ([PR #1880](https://github.com/versatica/mediasoup/pull/1880)).
8
+
9
+ ### 3.24.1
10
+
11
+ - Worker: Don't check `ignoredSubchannel` in piped `DataConsumers` ([PR #1879](https://github.com/versatica/mediasoup/pull/1879)).
12
+
5
13
  ### 3.24.0
6
14
 
7
15
  - `DataProducer.send()`: Add `ignoredSubchannel` optional argument ([PR #1877](https://github.com/versatica/mediasoup/pull/1877)).
@@ -967,8 +967,8 @@ test('transport.consumeData() for a pipe DataProducer succeeds with subchannels'
967
967
  /* ppid */ undefined,
968
968
  /* subchannels */ [125],
969
969
  /* requiredSubchannel */ 666);
970
- // The `pipeDataConsumer` is subscribed to subchannel 666 so it must not
971
- // allow the message to pass through.
970
+ // The final `directDataConsumer` is subscribed to subchannel 666 so it must
971
+ // not receive the message.
972
972
  directDataProducer.send('hello5',
973
973
  /* ppid */ undefined,
974
974
  /* subchannels */ [123],
@@ -979,7 +979,7 @@ test('transport.consumeData() for a pipe DataProducer succeeds with subchannels'
979
979
  directDataProducer.send('hello6',
980
980
  /* ppid */ undefined,
981
981
  /* subchannels */ undefined,
982
- /* requiredSubchannel */ undefined,
982
+ /* requiredSubchannel */ 123,
983
983
  /* ignoredSubchannel */ 777);
984
984
  // Send a message without subchannels so it's guaranteed that it will reach
985
985
  // the final `directDataConsumer`. And wait for reception of this message so
@@ -997,6 +997,63 @@ test('transport.consumeData() for a pipe DataProducer succeeds with subchannels'
997
997
  });
998
998
  expect(receivedMessages).toStrictEqual(['hello1', 'hello2', 'bye']);
999
999
  }, 2000);
1000
+ test('transport.consumeData() for a pipe DataProducer succeeds with ignoredSubchannel', async () => {
1001
+ const directTransport1 = await ctx.router1.createDirectTransport();
1002
+ const directDataProducer = await directTransport1.produceData({
1003
+ label: 'foo',
1004
+ protocol: 'bar',
1005
+ });
1006
+ const { pipeDataConsumer } = await ctx.router1.pipeToRouter({
1007
+ dataProducerId: directDataProducer.id,
1008
+ router: ctx.router2,
1009
+ });
1010
+ const directTransport2 = await ctx.router2.createDirectTransport();
1011
+ const directDataConsumer1 = await directTransport2.consumeData({
1012
+ dataProducerId: directDataProducer.id,
1013
+ subchannels: [111],
1014
+ });
1015
+ const directDataConsumer2 = await directTransport2.consumeData({
1016
+ dataProducerId: directDataProducer.id,
1017
+ subchannels: [222],
1018
+ });
1019
+ // A pipe DataConsumer must be subscribed to the union of the subchannels of
1020
+ // all the DataConsumers behind it.
1021
+ await pipeDataConsumer.setSubchannels([111, 222]);
1022
+ const receivedMessages1 = [];
1023
+ const receivedMessages2 = [];
1024
+ // Resolved once both final DataConsumers have received the last sent message.
1025
+ const allReceived = Promise.all([
1026
+ [directDataConsumer1, receivedMessages1],
1027
+ [directDataConsumer2, receivedMessages2],
1028
+ ].map(([dataConsumer, receivedMessages]) => new Promise(resolve => {
1029
+ dataConsumer.on('message', message => {
1030
+ const receivedMessage = message.toString('utf8');
1031
+ receivedMessages.push(receivedMessage);
1032
+ // Resolve once the last sent message is received.
1033
+ if (receivedMessage === 'bye') {
1034
+ resolve();
1035
+ }
1036
+ });
1037
+ })));
1038
+ // The `pipeDataConsumer` is subscribed to subchannel 111, but being a pipe
1039
+ // DataConsumer it must not apply `ignoredSubchannel` itself. Otherwise it
1040
+ // would drop the message for every DataConsumer behind it rather than just
1041
+ // for `directDataConsumer1`. `ignoredSubchannel` travels within the message
1042
+ // and is applied by the final DataConsumers, so `directDataConsumer2` must
1043
+ // still receive it.
1044
+ directDataProducer.send('hello1',
1045
+ /* ppid */ undefined,
1046
+ /* subchannels */ undefined,
1047
+ /* requiredSubchannel */ undefined,
1048
+ /* ignoredSubchannel */ 111);
1049
+ // Send a message without subchannels so it's guaranteed that it will reach
1050
+ // both final DataConsumers. And wait for reception of this message so at
1051
+ // this time we know whether the previous one reached them.
1052
+ directDataProducer.send('bye');
1053
+ await allReceived;
1054
+ expect(receivedMessages1).toStrictEqual(['bye']);
1055
+ expect(receivedMessages2).toStrictEqual(['hello1', 'bye']);
1056
+ }, 2000);
1000
1057
  test('dataProducer.close() is transmitted to pipe DataConsumer', async () => {
1001
1058
  const { pipeDataProducer } = await ctx.router1.pipeToRouter({
1002
1059
  dataProducerId: ctx.dataProducer.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasoup",
3
- "version": "3.24.0",
3
+ "version": "3.24.2",
4
4
  "description": "Cutting Edge WebRTC Video Conferencing",
5
5
  "contributors": [
6
6
  "Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)",
@@ -121,22 +121,22 @@
121
121
  "@types/debug": "^4.1.13",
122
122
  "@types/ini": "^4.1.1",
123
123
  "@types/jest": "^30.0.0",
124
- "@types/node": "^26.1.2",
125
- "eslint": "^10.8.0",
124
+ "@types/node": "^26.2.0",
125
+ "eslint": "^10.8.1",
126
126
  "eslint-config-prettier": "^10.1.8",
127
127
  "eslint-plugin-jest": "^29.16.0",
128
128
  "eslint-plugin-prettier": "^5.5.6",
129
129
  "globals": "^17.9.0",
130
130
  "ini": "^7.0.0",
131
131
  "jest": "^30.4.2",
132
- "knip": "^6.31.0",
133
- "marked": "^18.0.7",
132
+ "knip": "^6.32.0",
133
+ "marked": "^18.0.9",
134
134
  "open-cli": "^9.0.0",
135
135
  "pick-port": "^2.2.1",
136
136
  "prettier": "^3.9.6",
137
137
  "ts-jest": "^29.4.12",
138
138
  "typescript": "^6.0.3",
139
- "typescript-eslint": "^8.65.0",
139
+ "typescript-eslint": "^8.66.0",
140
140
  "werift-sctp": "^0.0.11"
141
141
  }
142
142
  }
@@ -93,6 +93,18 @@ namespace RTC
93
93
  void SctpBufferedAmountLow(uint32_t bufferedAmount) const;
94
94
  void SctpSendBufferFull() const;
95
95
  void DataProducerClosed();
96
+ /**
97
+ * Verifies if given subchannels would allow a message to be delivered to
98
+ * the DataConsumer endpoint or not.
99
+ *
100
+ * @remarks
101
+ * - This method must be called before invoking `SendMessage()` if subchannels
102
+ * are given to `SendMessage()`.
103
+ */
104
+ bool VerifySubchannels(
105
+ const std::vector<uint16_t>& subchannels,
106
+ std::optional<uint16_t> requiredSubchannel,
107
+ std::optional<uint16_t> ignoredSubchannel) const;
96
108
  bool SendMessage(
97
109
  RTC::SCTP::Message message,
98
110
  std::vector<uint16_t>& subchannels,
@@ -21,9 +21,9 @@
21
21
  }
22
22
  },
23
23
  "node_modules/brace-expansion": {
24
- "version": "5.0.8",
25
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
26
- "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
24
+ "version": "5.0.9",
25
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
26
+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "balanced-match": "^4.0.2"
@@ -105,9 +105,9 @@
105
105
  "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="
106
106
  },
107
107
  "brace-expansion": {
108
- "version": "5.0.8",
109
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
110
- "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
108
+ "version": "5.0.9",
109
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
110
+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
111
111
  "requires": {
112
112
  "balanced-match": "^4.0.2"
113
113
  }
@@ -482,96 +482,93 @@ namespace RTC
482
482
  this->listener->OnDataConsumerDataProducerClosed(this);
483
483
  }
484
484
 
485
- bool DataConsumer::SendMessage(
486
- RTC::SCTP::Message message,
487
- std::vector<uint16_t>& subchannels,
485
+ bool DataConsumer::VerifySubchannels(
486
+ const std::vector<uint16_t>& subchannels,
488
487
  std::optional<uint16_t> requiredSubchannel,
489
- std::optional<uint16_t> ignoredSubchannel,
490
- const onQueuedCallback* cb)
488
+ std::optional<uint16_t> ignoredSubchannel) const
491
489
  {
492
490
  MS_TRACE();
493
491
 
494
- if (!IsActive())
492
+ // Only verify subchannels if this is not a piped DataConsumer or if this
493
+ // is a piped DataConsumer and it's subscribed to at least one subchannel.
494
+ const bool verifySubchannels = !this->pipe || !this->subchannels.empty();
495
+
496
+ if (!verifySubchannels)
495
497
  {
496
- if (cb)
497
- {
498
- (*cb)(false, false);
499
- delete cb;
500
- }
498
+ return true;
499
+ }
501
500
 
501
+ // If a required subchannel is given, verify that this data consumer is
502
+ // subscribed to it.
503
+ if (requiredSubchannel.has_value() && !this->subchannels.contains(requiredSubchannel.value()))
504
+ {
502
505
  return false;
503
506
  }
504
507
 
505
- // Only verify subchannels if this is not a piped DataConsumer or if this is
506
- // a piped DataConsumer and it's subscribed to at least one subchannel.
507
- const bool verifySubchannels = !this->pipe || !this->subchannels.empty();
508
+ // If an ignored subchannel is given, verify that this data consumer is
509
+ // not subscribed to it, otherwise don't send this message to it.
510
+ // NOTE: We don't check this in piped DataConsumers on purpose. Otherwise
511
+ // a message generated by peer X and forwarded to all peers in another
512
+ // host wouldn't pass this filter in the local PipeTransport.
513
+ if (!this->pipe && ignoredSubchannel.has_value() && this->subchannels.contains(ignoredSubchannel.value()))
514
+ {
515
+ return false;
516
+ }
508
517
 
509
- if (verifySubchannels)
518
+ // If subchannels are given, verify that this data consumer is subscribed
519
+ // to at least one of them.
520
+ if (!subchannels.empty())
510
521
  {
511
- // If a required subchannel is given, verify that this data consumer is
512
- // subscribed to it.
513
- if (requiredSubchannel.has_value() && !this->subchannels.contains(requiredSubchannel.value()))
522
+ bool subchannelMatched{ false };
523
+
524
+ for (const auto subchannel : subchannels)
514
525
  {
515
- if (cb)
526
+ if (this->subchannels.contains(subchannel))
516
527
  {
517
- (*cb)(false, false);
518
- delete cb;
519
- }
528
+ subchannelMatched = true;
520
529
 
521
- return false;
530
+ break;
531
+ }
522
532
  }
523
533
 
524
- // If an ignored subchannel is given, verify that this data consumer is not
525
- // subscribed to it, otherwise don't send this message to it.
526
- if (ignoredSubchannel.has_value() && this->subchannels.contains(ignoredSubchannel.value()))
534
+ if (!subchannelMatched)
527
535
  {
528
- if (cb)
529
- {
530
- (*cb)(false, false);
531
- delete cb;
532
- }
533
-
534
536
  return false;
535
537
  }
538
+ }
536
539
 
537
- // If subchannels are given, verify that this data consumer is subscribed
538
- // to at least one of them.
539
- if (!subchannels.empty())
540
- {
541
- bool subchannelMatched{ false };
542
-
543
- for (const auto subchannel : subchannels)
544
- {
545
- if (this->subchannels.contains(subchannel))
546
- {
547
- subchannelMatched = true;
548
-
549
- break;
550
- }
551
- }
540
+ return true;
541
+ }
552
542
 
553
- if (!subchannelMatched)
554
- {
555
- if (cb)
556
- {
557
- (*cb)(false, false);
558
- delete cb;
559
- }
543
+ bool DataConsumer::SendMessage(
544
+ RTC::SCTP::Message message,
545
+ std::vector<uint16_t>& subchannels,
546
+ std::optional<uint16_t> requiredSubchannel,
547
+ std::optional<uint16_t> ignoredSubchannel,
548
+ const onQueuedCallback* cb)
549
+ {
550
+ MS_TRACE();
560
551
 
561
- return false;
562
- }
552
+ if (!IsActive())
553
+ {
554
+ if (cb)
555
+ {
556
+ (*cb)(false, false);
557
+ delete cb;
563
558
  }
559
+
560
+ return false;
564
561
  }
565
562
 
566
- // If this is a piped DataConsumer, encode given subchannels and required
567
- // subchannel at the beginning of the message payload so the receiving
568
- // PipeTransport can decode them and apply them to its own DataConsumers.
563
+ // If this is a piped DataConsumer, encode given subchannels at the beginning
564
+ // of the message payload so the receiving PipeTransport can decode them and
565
+ // apply them to its own DataConsumers.
569
566
  if (this->pipe)
570
567
  {
571
- // If subchannels were verified, encode just those this DataConsumer is
572
- // subscribed to. Otherwise the receiving Router would deliver the message to
568
+ // Encode just those subchannels this DataConsumer is subscribed to.
569
+ // Otherwise the receiving Router would deliver the message to
573
570
  // DataConsumers subscribed to subchannels that this pipe does not carry.
574
- const bool reduceSubchannels = verifySubchannels && !subchannels.empty();
571
+ const bool reduceSubchannels = !this->subchannels.empty();
575
572
 
576
573
  std::vector<uint16_t> reducedSubchannels;
577
574
 
@@ -917,44 +917,57 @@ namespace RTC
917
917
 
918
918
  auto& dataConsumers = this->mapDataProducerDataConsumers.at(dataProducer);
919
919
 
920
- if (!dataConsumers.empty())
920
+ const auto getStreamId = [](const RTC::DataConsumer* dataConsumer) -> uint16_t
921
921
  {
922
- const auto numDataConsumers = dataConsumers.size();
922
+ return dataConsumer->GetType() == DataConsumer::Type::SCTP
923
+ ? dataConsumer->GetSctpStreamParameters().streamId
924
+ : 0;
925
+ };
923
926
 
924
- for (auto* dataConsumer : dataConsumers)
927
+ // NOTE: We don't send the message to a matching DataConsumer right away.
928
+ // Instead we hold it as pending until we know whether there is another
929
+ // matching DataConsumer after it. This way just the last matching one needs
930
+ // no message clone since it can take ownership of the original message.
931
+ RTC::DataConsumer* pendingDataConsumer{ nullptr };
932
+
933
+ for (auto* dataConsumer : dataConsumers)
934
+ {
935
+ // Verify subchannels first to avoid cloning the message needlessly.
936
+ if (!dataConsumer->IsActive() || !dataConsumer->VerifySubchannels(subchannels, requiredSubchannel, ignoredSubchannel))
925
937
  {
926
- const uint16_t streamId =
927
- (dataConsumer->GetType() == DataConsumer::Type::SCTP
928
- ? dataConsumer->GetSctpStreamParameters().streamId
929
- : 0);
938
+ continue;
939
+ }
930
940
 
931
- if (numDataConsumers == 1)
932
- {
933
- // We must update the message`s `streamId` for each destination
934
- // DataConsumer.
935
- // NOTE: clang-tidy doesn't understand that we are only doing this
936
- // once in the original `message`.
937
- // NOLINTNEXTLINE(clang-analyzer-cplusplus.Move, bugprone-use-after-move, hicpp-invalid-access-moved)
938
- message.SetStreamId(streamId);
939
-
940
- dataConsumer->SendMessage(
941
- std::move(message), subchannels, requiredSubchannel, ignoredSubchannel);
942
- }
943
- // NOTE: Here we are cloning the message before passing it to each
944
- // DataConsumer because each DataConsumer will pass std::move(message)
945
- // internally to its SCTP association.
946
- else
947
- {
948
- auto clonedMessage = message.Clone();
941
+ // There is a matching DataConsumer after the pending one, so the pending
942
+ // one must be given a clone of the message.
943
+ // NOTE: Here we are cloning the message before passing it to the
944
+ // DataConsumer because each DataConsumer will pass std::move(message)
945
+ // internally to its SCTP association.
946
+ if (pendingDataConsumer)
947
+ {
948
+ auto clonedMessage = message.Clone();
949
949
 
950
- // We must update the message`s `streamId` for each destination
951
- // DataConsumer.
952
- clonedMessage.SetStreamId(streamId);
950
+ // We must update the message`s `streamId` for each destination
951
+ // DataConsumer.
952
+ clonedMessage.SetStreamId(getStreamId(pendingDataConsumer));
953
953
 
954
- dataConsumer->SendMessage(
955
- std::move(clonedMessage), subchannels, requiredSubchannel, ignoredSubchannel);
956
- }
954
+ pendingDataConsumer->SendMessage(
955
+ std::move(clonedMessage), subchannels, requiredSubchannel, ignoredSubchannel);
957
956
  }
957
+
958
+ pendingDataConsumer = dataConsumer;
959
+ }
960
+
961
+ // The last matching DataConsumer (if any) takes ownership of the original
962
+ // message.
963
+ if (pendingDataConsumer)
964
+ {
965
+ // We must update the message`s `streamId` for each destination
966
+ // DataConsumer.
967
+ message.SetStreamId(getStreamId(pendingDataConsumer));
968
+
969
+ pendingDataConsumer->SendMessage(
970
+ std::move(message), subchannels, requiredSubchannel, ignoredSubchannel);
958
971
  }
959
972
  }
960
973