arnacon-webrtc-service 0.2.1 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arnacon-webrtc-service",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Arnacon WebRTC core runtime and service modules",
5
5
  "main": "./webRTCservice/core.js",
6
6
  "type": "commonjs",
@@ -118,6 +118,51 @@ class MidLifecyclePeerConnection extends FakePeerConnection {
118
118
  }
119
119
  }
120
120
 
121
+ class DuplicateTrackOnRecoveryPeerConnection extends FakePeerConnection {
122
+ constructor({ audioMid = "2", ...opts } = {}) {
123
+ super(opts);
124
+ this.transceivers = [
125
+ { kind: "application", mid: "0", setDirection() {}, sender: { replaceTrack: async () => {} } },
126
+ { kind: "audio", mid: audioMid, setDirection() {}, sender: { replaceTrack: async () => {}, track: null } },
127
+ ];
128
+ this._remoteOfferAudioMid = null;
129
+ }
130
+
131
+ _extractAudioMid(sdp = "") {
132
+ const sections = String(sdp).split(/\r?\nm=/);
133
+ for (const rawSection of sections) {
134
+ const section = rawSection.startsWith("m=") ? rawSection : `m=${rawSection}`;
135
+ if (!/^m=audio\b/m.test(section)) continue;
136
+ const mid = section.match(/^a=mid:([^\r\n]+)/m)?.[1];
137
+ if (mid) return String(mid);
138
+ }
139
+ return null;
140
+ }
141
+
142
+ _hasAudioMid(mid) {
143
+ return this.transceivers.some((t) => t.kind === "audio" && String(t.mid || "") === String(mid || ""));
144
+ }
145
+
146
+ addTrack(track) {
147
+ const audio = this.transceivers.find((t) => t.kind === "audio");
148
+ if (audio?.sender?.track === track) {
149
+ throw new Error("Track already added");
150
+ }
151
+ if (audio?.sender) audio.sender.track = track;
152
+ return { kind: track?.kind || "audio" };
153
+ }
154
+
155
+ async setRemoteDescription(d) {
156
+ if (d?.type === "offer") {
157
+ this._remoteOfferAudioMid = this._extractAudioMid(d?.sdp || "");
158
+ if (this._remoteOfferAudioMid && !this._hasAudioMid(this._remoteOfferAudioMid)) {
159
+ throw new Error(`Transceiver with mid=${this._remoteOfferAudioMid} not found`);
160
+ }
161
+ }
162
+ this.remoteDescription = d;
163
+ }
164
+ }
165
+
121
166
  function deepLifecyclePrimitives() {
122
167
  const base = fakePrimitives();
123
168
  const callSdpUseCases = new CallSdpUseCases({
@@ -474,3 +519,42 @@ test("DEEP BUG REPRO: stale audio MID can fail during setLocalDescription", asyn
474
519
  "redial offer should not wedge even when MID mismatch surfaces at setLocalDescription time",
475
520
  );
476
521
  });
522
+
523
+ test("DEEP BUG REPRO: recovery must not throw when local track is already bound", async () => {
524
+ const signaling = new FakeSignaling();
525
+ const localTrack = { kind: "audio" };
526
+ const pc = new DuplicateTrackOnRecoveryPeerConnection({
527
+ audioMid: "2",
528
+ answerSdp: "v=0\r\nm=audio 9 UDP/TLS/RTP/SAVPF 8\r\na=mid:1\r\na=sendrecv\r\n",
529
+ });
530
+ pc.transceivers.find((t) => t.kind === "audio").sender.track = localTrack;
531
+ const session = {
532
+ sessionId: "alice|bob",
533
+ callerEns: "alice.secnum.global",
534
+ toIdentity: "bob.secnum.global",
535
+ peerConnection: pc,
536
+ localAudioTrack: localTrack,
537
+ };
538
+ const neg = new WebRtcNegotiation({
539
+ id: "alice",
540
+ endpoint: "alice.secnum.global",
541
+ session,
542
+ signaling,
543
+ primitives: deepLifecyclePrimitives(),
544
+ logger: silentLogger,
545
+ });
546
+
547
+ const redialOfferWithAudioMid =
548
+ "v=0\r\n" +
549
+ "a=group:BUNDLE 0 1\r\n" +
550
+ "m=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n" +
551
+ "a=mid:0\r\n" +
552
+ "m=audio 9 UDP/TLS/RTP/SAVPF 8\r\n" +
553
+ "a=mid:1\r\n" +
554
+ "a=sendrecv\r\n";
555
+
556
+ await assert.doesNotReject(
557
+ () => neg.applyOffer({ mode: "ring", payload: { sdp: redialOfferWithAudioMid } }),
558
+ "recovery should tolerate already-bound tracks and continue",
559
+ );
560
+ });
@@ -254,6 +254,17 @@ class WebRtcNegotiation extends CallNegotiationPort {
254
254
  return /Transceiver with mid=\d+ not found/i.test(msg);
255
255
  }
256
256
 
257
+ _isTrackAlreadyAddedError(err) {
258
+ const msg = String(err?.message || "");
259
+ return /Track already added/i.test(msg);
260
+ }
261
+
262
+ _isTrackBoundToAudioTransceiver(pc, track) {
263
+ if (!track) return false;
264
+ const audioTransceivers = pc.getTransceivers?.().filter((t) => t.kind === "audio") || [];
265
+ return audioTransceivers.some((t) => t?.sender?.track === track);
266
+ }
267
+
257
268
  _primeAudioTransceiver(transceiver, track) {
258
269
  if (!transceiver) return;
259
270
  try { transceiver.setDirection?.("sendrecv"); } catch (_) {}
@@ -303,14 +314,25 @@ class WebRtcNegotiation extends CallNegotiationPort {
303
314
  if (!this._offerHasAudioMLine(sdp)) return;
304
315
  const force = opts.force === true;
305
316
  const audioTransceiver = pc.getTransceivers?.().find((t) => t.kind === "audio");
306
- if (audioTransceiver && !force) return;
317
+ if (audioTransceiver && !force) {
318
+ this._primeAudioTransceiver(audioTransceiver, this.session.localAudioTrack || null);
319
+ return;
320
+ }
307
321
 
308
322
  if (!this.session.localAudioTrack) {
309
323
  this.session.localAudioTrack = new this.MediaStreamTrack({ kind: "audio" });
310
324
  }
311
- pc.addTrack?.(this.session.localAudioTrack);
325
+ const track = this.session.localAudioTrack;
326
+ const alreadyBound = this._isTrackBoundToAudioTransceiver(pc, track);
327
+ if (!alreadyBound && typeof pc.addTrack === "function") {
328
+ try {
329
+ pc.addTrack(track);
330
+ } catch (err) {
331
+ if (!this._isTrackAlreadyAddedError(err)) throw err;
332
+ }
333
+ }
312
334
  const latestAudio = pc.getTransceivers?.().find((t) => t.kind === "audio");
313
- if (latestAudio) this._primeAudioTransceiver(latestAudio, this.session.localAudioTrack);
335
+ if (latestAudio) this._primeAudioTransceiver(latestAudio, track);
314
336
  }
315
337
 
316
338
  // The caller's client offered a ring and we are connected: ack it so the