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
|
@@ -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)
|
|
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
|
-
|
|
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,
|
|
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
|