arnacon-webrtc-service 0.2.3 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arnacon-webrtc-service",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Arnacon WebRTC core runtime and service modules",
5
5
  "main": "./webRTCservice/core.js",
6
6
  "type": "commonjs",
@@ -4,6 +4,7 @@ const assert = require("node:assert/strict");
4
4
  const { WebRtcNegotiation } = require("../negotiation/WebRtcNegotiation");
5
5
  const { FakeSignaling, FakePeerConnection, fakePrimitives, silentLogger } = require("./fakes");
6
6
  const { CallSdpUseCases } = require("../../useCases/CallSdpUseCases");
7
+ const { addIceCandidates: addIceCandidatesUtil } = require("../../../media/negotiation/SdpUtils");
7
8
 
8
9
  function build({ offerSdp, answerSdp } = {}) {
9
10
  const signaling = new FakeSignaling();
@@ -234,6 +235,28 @@ class MidMapLagPeerConnection extends FakePeerConnection {
234
235
  }
235
236
  }
236
237
 
238
+ class CandidateMLineStrictPeerConnection extends FakePeerConnection {
239
+ constructor(opts = {}) {
240
+ super(opts);
241
+ this._remoteMLineCount = 0;
242
+ this.appliedCandidates = [];
243
+ }
244
+
245
+ async setRemoteDescription(d) {
246
+ this.remoteDescription = d;
247
+ const sdp = String(d?.sdp || "");
248
+ this._remoteMLineCount = (sdp.match(/^m=/gm) || []).length;
249
+ }
250
+
251
+ async addIceCandidate(candidate) {
252
+ const idx = Number(candidate?.sdpMLineIndex);
253
+ if (Number.isFinite(idx) && (idx < 0 || idx >= this._remoteMLineCount)) {
254
+ throw new Error("m line not found");
255
+ }
256
+ this.appliedCandidates.push(candidate);
257
+ }
258
+ }
259
+
237
260
  function deepLifecyclePrimitives() {
238
261
  const base = fakePrimitives();
239
262
  const callSdpUseCases = new CallSdpUseCases({
@@ -250,6 +273,12 @@ function deepLifecyclePrimitives() {
250
273
  ...base,
251
274
  ensureLocalAudioTrack: (...args) => callSdpUseCases.ensureLocalAudioTrack(...args),
252
275
  createAnswerSdp: (...args) => callSdpUseCases.createAnswerSdp(...args),
276
+ addIceCandidates: (...args) =>
277
+ addIceCandidatesUtil(...args, class RTCIceCandidate {
278
+ constructor(init = {}) {
279
+ Object.assign(this, init);
280
+ }
281
+ }),
253
282
  };
254
283
  }
255
284
 
@@ -828,3 +857,46 @@ test("DEEP BUG REPRO: ice-restart after reuse survives stale mid map", async ()
828
857
  "ice-restart path should recover from stale reused mid mappings",
829
858
  );
830
859
  });
860
+
861
+ test("DEEP BUG REPRO: applyOffer ignores stale ICE candidate m-line mismatch", async () => {
862
+ const signaling = new FakeSignaling();
863
+ const pc = new CandidateMLineStrictPeerConnection({
864
+ answerSdp: "v=0\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\na=mid:0\r\n",
865
+ });
866
+ const session = {
867
+ sessionId: "alice|bob|mline",
868
+ callerEns: "alice.secnum.global",
869
+ toIdentity: "bob.secnum.global",
870
+ peerConnection: pc,
871
+ localAudioTrack: { kind: "audio" },
872
+ };
873
+ const neg = new WebRtcNegotiation({
874
+ id: "alice",
875
+ endpoint: "alice.secnum.global",
876
+ session,
877
+ signaling,
878
+ primitives: deepLifecyclePrimitives(),
879
+ logger: silentLogger,
880
+ });
881
+ const dataOnlyOffer =
882
+ "v=0\r\n" +
883
+ "a=group:BUNDLE 0\r\n" +
884
+ "m=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n" +
885
+ "a=mid:0\r\n";
886
+ await assert.doesNotReject(
887
+ () =>
888
+ neg.applyOffer({
889
+ mode: "ring",
890
+ payload: {
891
+ sdp: dataOnlyOffer,
892
+ candidates: [
893
+ { candidate: "candidate:1 1 udp 1 1.1.1.1 11111 typ host", sdpMid: "0", sdpMLineIndex: 0 },
894
+ { candidate: "candidate:2 1 udp 1 2.2.2.2 22222 typ host", sdpMid: "1", sdpMLineIndex: 1 },
895
+ ],
896
+ },
897
+ }),
898
+ "stale candidate with missing m-line must be ignored instead of failing offer ingress",
899
+ );
900
+ assert.equal(pc.appliedCandidates.length, 1, "valid candidate should still be applied");
901
+ assert.equal(pc.appliedCandidates[0].sdpMLineIndex, 0);
902
+ });
@@ -93,10 +93,20 @@ function logSdp(sessionId, label, sdp, logger = console) {
93
93
 
94
94
  async function addIceCandidates(pc, candidates, RTCIceCandidate) {
95
95
  let added = 0;
96
+ const isMLineMismatch = (err) => {
97
+ const msg = String(err?.message || "");
98
+ return /m[-\s]?line not found/i.test(msg);
99
+ };
96
100
  for (const c of (candidates || [])) {
97
101
  if (c && c.candidate) {
98
- await pc.addIceCandidate(new RTCIceCandidate(c));
99
- added++;
102
+ try {
103
+ await pc.addIceCandidate(new RTCIceCandidate(c));
104
+ added++;
105
+ } catch (err) {
106
+ // Reused sessions can receive stale trickle candidates that refer to
107
+ // an m-line no longer present in the latest negotiated SDP.
108
+ if (!isMLineMismatch(err)) throw err;
109
+ }
100
110
  }
101
111
  }
102
112
  return added;