arnacon-webrtc-service 0.2.5 → 0.2.7
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 +1 -1
- package/webRTCservice/modules/calls/poly/__tests__/WebRtcNegotiation.test.js +54 -2
- package/webRTCservice/modules/calls/poly/__tests__/WebRtcNegotiation.werift.integration.test.js +57 -0
- package/webRTCservice/modules/calls/poly/negotiation/WebRtcNegotiation.js +13 -5
- package/webRTCservice/modules/media/negotiation/SdpUtils.js +2 -1
- package/webRTCservice/webRTCmanager.js +29 -0
package/package.json
CHANGED
|
@@ -235,6 +235,15 @@ class MidMapLagPeerConnection extends FakePeerConnection {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
class MidMapLagTrackCreateFailsPeerConnection extends MidMapLagPeerConnection {
|
|
239
|
+
addTransceiver(kindOrTrack) {
|
|
240
|
+
if (kindOrTrack && typeof kindOrTrack === "object" && kindOrTrack.kind === "audio") {
|
|
241
|
+
throw new Error("Track already added");
|
|
242
|
+
}
|
|
243
|
+
return super.addTransceiver(kindOrTrack);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
238
247
|
class CandidateMLineStrictPeerConnection extends FakePeerConnection {
|
|
239
248
|
constructor(opts = {}) {
|
|
240
249
|
super(opts);
|
|
@@ -260,9 +269,15 @@ class CandidateMLineStrictPeerConnection extends FakePeerConnection {
|
|
|
260
269
|
class ParserMLineRecoveryPeerConnection extends FakePeerConnection {
|
|
261
270
|
constructor(opts = {}) {
|
|
262
271
|
super(opts);
|
|
272
|
+
const makeSender = () => ({
|
|
273
|
+
track: null,
|
|
274
|
+
async replaceTrack(nextTrack) {
|
|
275
|
+
this.track = nextTrack || null;
|
|
276
|
+
},
|
|
277
|
+
});
|
|
263
278
|
this.transceivers = [
|
|
264
|
-
{ kind: "application", mid: "0", setDirection() {}, sender:
|
|
265
|
-
{ kind: "audio", mid: "1", setDirection() {}, sender:
|
|
279
|
+
{ kind: "application", mid: "0", setDirection() {}, sender: makeSender() },
|
|
280
|
+
{ kind: "audio", mid: "1", setDirection() {}, sender: makeSender() },
|
|
266
281
|
];
|
|
267
282
|
this.failOnce = true;
|
|
268
283
|
}
|
|
@@ -777,6 +792,43 @@ test("DEEP BUG REPRO: repeated decline/reuse can advance to mid=2 then mid=3", a
|
|
|
777
792
|
);
|
|
778
793
|
});
|
|
779
794
|
|
|
795
|
+
test("DEEP BUG REPRO: recovery falls back to addTransceiver('audio') when addTransceiver(track) fails", async () => {
|
|
796
|
+
const signaling = new FakeSignaling();
|
|
797
|
+
const localTrack = { kind: "audio" };
|
|
798
|
+
const pc = new MidMapLagTrackCreateFailsPeerConnection({
|
|
799
|
+
initialAudioMid: "1",
|
|
800
|
+
answerSdp: "v=0\r\nm=audio 9 UDP/TLS/RTP/SAVPF 8\r\na=mid:2\r\na=sendrecv\r\n",
|
|
801
|
+
});
|
|
802
|
+
pc.transceivers.find((t) => t.kind === "audio").sender.track = localTrack;
|
|
803
|
+
const session = {
|
|
804
|
+
sessionId: "alice|bob|mid2-fallback",
|
|
805
|
+
callerEns: "alice.secnum.global",
|
|
806
|
+
toIdentity: "bob.secnum.global",
|
|
807
|
+
peerConnection: pc,
|
|
808
|
+
localAudioTrack: localTrack,
|
|
809
|
+
};
|
|
810
|
+
const neg = new WebRtcNegotiation({
|
|
811
|
+
id: "alice",
|
|
812
|
+
endpoint: "alice.secnum.global",
|
|
813
|
+
session,
|
|
814
|
+
signaling,
|
|
815
|
+
primitives: deepLifecyclePrimitives(),
|
|
816
|
+
logger: silentLogger,
|
|
817
|
+
});
|
|
818
|
+
const offerWithAudioMid2 =
|
|
819
|
+
"v=0\r\n" +
|
|
820
|
+
"a=group:BUNDLE 0 2\r\n" +
|
|
821
|
+
"m=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n" +
|
|
822
|
+
"a=mid:0\r\n" +
|
|
823
|
+
"m=audio 9 UDP/TLS/RTP/SAVPF 8\r\n" +
|
|
824
|
+
"a=mid:2\r\n" +
|
|
825
|
+
"a=sendrecv\r\n";
|
|
826
|
+
await assert.doesNotReject(
|
|
827
|
+
() => neg.applyOffer({ mode: "ring", payload: { sdp: offerWithAudioMid2 } }),
|
|
828
|
+
"mid recovery should succeed even if addTransceiver(track) fails on reused track",
|
|
829
|
+
);
|
|
830
|
+
});
|
|
831
|
+
|
|
780
832
|
test("DEEP MATRIX: createAnswer recovery handles missing mids 1/2/3", async () => {
|
|
781
833
|
for (const mid of ["1", "2", "3"]) {
|
|
782
834
|
const signaling = new FakeSignaling();
|
package/webRTCservice/modules/calls/poly/__tests__/WebRtcNegotiation.werift.integration.test.js
CHANGED
|
@@ -84,6 +84,37 @@ const inactiveEndCallOfferSdp =
|
|
|
84
84
|
"a=rtcp-mux\r\n" +
|
|
85
85
|
"a=rtpmap:8 PCMA/8000\r\n";
|
|
86
86
|
|
|
87
|
+
const activeAudioOfferMid2Sdp =
|
|
88
|
+
"v=0\r\n" +
|
|
89
|
+
"o=- 83392955 0 IN IP4 0.0.0.0\r\n" +
|
|
90
|
+
"s=-\r\n" +
|
|
91
|
+
"t=0 0\r\n" +
|
|
92
|
+
"a=group:BUNDLE 0 2\r\n" +
|
|
93
|
+
"a=extmap-allow-mixed\r\n" +
|
|
94
|
+
"a=msid-semantic:WMS *\r\n" +
|
|
95
|
+
"m=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n" +
|
|
96
|
+
"c=IN IP4 0.0.0.0\r\n" +
|
|
97
|
+
"a=ice-ufrag:ddcb\r\n" +
|
|
98
|
+
"a=ice-pwd:701e83d2d6de10d14b0eea\r\n" +
|
|
99
|
+
"a=ice-options:trickle\r\n" +
|
|
100
|
+
"a=fingerprint:sha-256 F8:C4:0C:11:3F:96:C8:6A:48:A6:88:42:28:86:2B:9D:77:11:E2:75:65:DE:81:F5:7E:CA:B5:AB:0A:6B:64:F1\r\n" +
|
|
101
|
+
"a=setup:passive\r\n" +
|
|
102
|
+
"a=mid:0\r\n" +
|
|
103
|
+
"a=sctp-port:5000\r\n" +
|
|
104
|
+
"a=max-message-size:65536\r\n" +
|
|
105
|
+
"m=audio 9 UDP/TLS/RTP/SAVPF 8\r\n" +
|
|
106
|
+
"c=IN IP4 0.0.0.0\r\n" +
|
|
107
|
+
"a=ice-ufrag:ddcb\r\n" +
|
|
108
|
+
"a=ice-pwd:701e83d2d6de10d14b0eea\r\n" +
|
|
109
|
+
"a=ice-options:trickle\r\n" +
|
|
110
|
+
"a=fingerprint:sha-256 F8:C4:0C:11:3F:96:C8:6A:48:A6:88:42:28:86:2B:9D:77:11:E2:75:65:DE:81:F5:7E:CA:B5:AB:0A:6B:64:F1\r\n" +
|
|
111
|
+
"a=setup:passive\r\n" +
|
|
112
|
+
"a=sendrecv\r\n" +
|
|
113
|
+
"a=mid:2\r\n" +
|
|
114
|
+
"a=rtcp:9 IN IP4 0.0.0.0\r\n" +
|
|
115
|
+
"a=rtcp-mux\r\n" +
|
|
116
|
+
"a=rtpmap:8 PCMA/8000\r\n";
|
|
117
|
+
|
|
87
118
|
const malformedAudioOfferSdp =
|
|
88
119
|
"v=0\r\n" +
|
|
89
120
|
"o=- 41053953 0 IN IP4 0.0.0.0\r\n" +
|
|
@@ -196,3 +227,29 @@ test("WERIFT REPLAY: ackEnd path then redial on same session", async () => {
|
|
|
196
227
|
await harness.dispose();
|
|
197
228
|
}
|
|
198
229
|
});
|
|
230
|
+
|
|
231
|
+
test("WERIFT REPLAY: decline/end then redial with audio mid=2 stays reusable", async () => {
|
|
232
|
+
const harness = buildWeriftNegotiationHarness({ withAudioTrack: true, withDataChannel: true });
|
|
233
|
+
try {
|
|
234
|
+
await assert.doesNotReject(() =>
|
|
235
|
+
harness.neg.applyOffer({
|
|
236
|
+
mode: "ring",
|
|
237
|
+
payload: { sdp: activeAudioOfferSdp, candidates: [candidateM0] },
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
await assert.doesNotReject(() =>
|
|
241
|
+
harness.neg.endCall({
|
|
242
|
+
mode: "remote",
|
|
243
|
+
payload: { type: "offer", sdp: inactiveEndCallOfferSdp },
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
await assert.doesNotReject(() =>
|
|
247
|
+
harness.neg.applyOffer({
|
|
248
|
+
mode: "ring",
|
|
249
|
+
payload: { sdp: activeAudioOfferMid2Sdp, candidates: [candidateM0] },
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
} finally {
|
|
253
|
+
await harness.dispose();
|
|
254
|
+
}
|
|
255
|
+
});
|
|
@@ -373,15 +373,23 @@ class WebRtcNegotiation extends CallNegotiationPort {
|
|
|
373
373
|
// Final fallback: try creating/binding a fresh audio transceiver if the
|
|
374
374
|
// implementation supports it.
|
|
375
375
|
if (typeof pc.addTransceiver === "function") {
|
|
376
|
+
let created = null;
|
|
376
377
|
try {
|
|
377
|
-
|
|
378
|
+
created = track
|
|
378
379
|
? pc.addTransceiver(track, { direction: "sendrecv" })
|
|
379
380
|
: pc.addTransceiver("audio", { direction: "sendrecv" });
|
|
380
|
-
if (created) {
|
|
381
|
-
try { created.mid = offerMid; } catch (_) {}
|
|
382
|
-
this._primeAudioTransceiver(created, track);
|
|
383
|
-
}
|
|
384
381
|
} catch (_) {}
|
|
382
|
+
// Some stacks reject addTransceiver(track) when that track is already
|
|
383
|
+
// attached; retry with kind-only to still create the missing MID slot.
|
|
384
|
+
if (!created) {
|
|
385
|
+
try {
|
|
386
|
+
created = pc.addTransceiver("audio", { direction: "sendrecv" });
|
|
387
|
+
} catch (_) {}
|
|
388
|
+
}
|
|
389
|
+
if (created) {
|
|
390
|
+
try { created.mid = offerMid; } catch (_) {}
|
|
391
|
+
this._primeAudioTransceiver(created, track);
|
|
392
|
+
}
|
|
385
393
|
}
|
|
386
394
|
|
|
387
395
|
if (!hasExactMid()) {
|
|
@@ -98,7 +98,8 @@ async function addIceCandidates(pc, candidates, RTCIceCandidate) {
|
|
|
98
98
|
return (
|
|
99
99
|
/m[-\s]?line not found/i.test(msg) ||
|
|
100
100
|
/iceParams/i.test(msg) ||
|
|
101
|
-
/media section/i.test(msg)
|
|
101
|
+
/media section/i.test(msg) ||
|
|
102
|
+
/reading 'kind'/i.test(msg)
|
|
102
103
|
);
|
|
103
104
|
};
|
|
104
105
|
for (const c of (candidates || [])) {
|
|
@@ -1129,6 +1129,17 @@ function onPeerConnected(sessionId) {
|
|
|
1129
1129
|
// No pending-bridge bookkeeping anymore — pairing is handled by the registry.
|
|
1130
1130
|
}
|
|
1131
1131
|
|
|
1132
|
+
function isRecoverableOfferIngressError(err) {
|
|
1133
|
+
const msg = String(err?.message || "");
|
|
1134
|
+
return (
|
|
1135
|
+
/Transceiver with mid=\d+ not found/i.test(msg) ||
|
|
1136
|
+
/m[-\s]?line not found/i.test(msg) ||
|
|
1137
|
+
/iceParams/i.test(msg) ||
|
|
1138
|
+
/media section/i.test(msg) ||
|
|
1139
|
+
/reading 'kind'/i.test(msg)
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1132
1143
|
/**
|
|
1133
1144
|
* Data channel ingress -> normalized leg events for the owning PolySession.
|
|
1134
1145
|
* Replaces SignalingMessageRouter phase-gating (leg states gate instead).
|
|
@@ -1208,6 +1219,24 @@ function onDataChannelMessage(sessionId, rawMessage, meta = {}) {
|
|
|
1208
1219
|
if (!event) return;
|
|
1209
1220
|
const ref = polyWebrtcRef(poly, session, channelRole);
|
|
1210
1221
|
poly.onIngress(ref, event).catch((err) => {
|
|
1222
|
+
if (action === "offer" && isRecoverableOfferIngressError(err) && !payload.__polyIngressRetriedOnce) {
|
|
1223
|
+
payload.__polyIngressRetriedOnce = true;
|
|
1224
|
+
(async () => {
|
|
1225
|
+
const latestSession = sessions.get(sessionId);
|
|
1226
|
+
if (!latestSession) return;
|
|
1227
|
+
const existing = polyForSession(latestSession);
|
|
1228
|
+
if (existing) {
|
|
1229
|
+
await polyRegistry.destroy(
|
|
1230
|
+
polyRegistry.keyForPair(existing.legs.a.endpoint, existing.legs.b.endpoint),
|
|
1231
|
+
"offer-recover-reset",
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
await onDcRing(sessionId, payload);
|
|
1235
|
+
})().catch((retryErr) => {
|
|
1236
|
+
console.error(`[${sessionId}] poly ingress (${action}) retry failed: ${retryErr.message}`);
|
|
1237
|
+
});
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1211
1240
|
console.error(`[${sessionId}] poly ingress (${action}) failed: ${err.message}`);
|
|
1212
1241
|
});
|
|
1213
1242
|
}
|