arnacon-webrtc-service 0.1.166 → 0.1.167
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/PolySession.js +37 -0
- package/webRTCservice/modules/calls/poly/__tests__/PolySession.test.js +34 -0
- package/webRTCservice/modules/calls/poly/__tests__/ReconcileRules.test.js +6 -0
- package/webRTCservice/modules/media/werift/PeerConnectionFactory.js +2 -2
- package/webRTCservice/webRTCmanager.js +9 -0
package/package.json
CHANGED
|
@@ -7,9 +7,22 @@
|
|
|
7
7
|
|
|
8
8
|
const { reconcile } = require("./ReconcileRules");
|
|
9
9
|
const { LEG_INTENTS } = require("./ports");
|
|
10
|
+
const { LEG_STATES, isActiveCall } = require("./states");
|
|
10
11
|
|
|
11
12
|
const MAX_RECONCILE_PASSES = 50;
|
|
12
13
|
|
|
14
|
+
// Intents that drive a call toward the peer. If one of these throws (e.g. the SIP
|
|
15
|
+
// INVITE is rejected during `ring`), the leg can no longer carry the call -> mark
|
|
16
|
+
// it FAILED so reconcile ends the caller (end-call reneg). Teardown intents are
|
|
17
|
+
// excluded: failing a teardown into more teardown would loop.
|
|
18
|
+
const FAIL_ON_ERROR_INTENTS = new Set([
|
|
19
|
+
LEG_INTENTS.RING,
|
|
20
|
+
LEG_INTENTS.CONNECT,
|
|
21
|
+
LEG_INTENTS.ANSWER,
|
|
22
|
+
LEG_INTENTS.ACK_CONNECTED,
|
|
23
|
+
LEG_INTENTS.ACK_RING,
|
|
24
|
+
]);
|
|
25
|
+
|
|
13
26
|
class PolySession {
|
|
14
27
|
constructor({ id, legA, legB, mediaController, rules = reconcile, teardownHooks = [], logger = console } = {}) {
|
|
15
28
|
if (!legA || !legB) throw new Error("PolySession requires two legs");
|
|
@@ -95,6 +108,7 @@ class PolySession {
|
|
|
95
108
|
for (const action of actions) {
|
|
96
109
|
await this._execute(action);
|
|
97
110
|
}
|
|
111
|
+
this._recoverFailedLegs();
|
|
98
112
|
}
|
|
99
113
|
} finally {
|
|
100
114
|
this._running = false;
|
|
@@ -129,10 +143,33 @@ class PolySession {
|
|
|
129
143
|
}
|
|
130
144
|
} catch (err) {
|
|
131
145
|
this.logger.error(`[${this.id}] intent ${action.intent} on leg ${leg.id} failed: ${err.message}`);
|
|
146
|
+
// A reaching intent failed -> the leg cannot carry the call. Fail it so
|
|
147
|
+
// reconcile drives the peer into an end-call reneg (a FAILED leg is a
|
|
148
|
+
// teardown trigger). Idempotent: no-op if already FAILED/torn down.
|
|
149
|
+
if (FAIL_ON_ERROR_INTENTS.has(action.intent) && leg.state !== LEG_STATES.FAILED) {
|
|
150
|
+
leg.setState(LEG_STATES.FAILED, { reason: `intent-failed:${action.intent}`, from: "self" });
|
|
151
|
+
}
|
|
132
152
|
}
|
|
133
153
|
return undefined;
|
|
134
154
|
}
|
|
135
155
|
|
|
156
|
+
// Once the call is over, a transportless leg left FAILED (e.g. a SIP INVITE that
|
|
157
|
+
// was rejected during ring) is stuck: FAILED is not rungable, so the next call
|
|
158
|
+
// would stall. Reset it to its idle (DISCONNECTED) so reconcile can re-drive it
|
|
159
|
+
// (the SIP leg rebuilds its INVITE per call). Scoped to SIP: a webrtc leg's
|
|
160
|
+
// FAILED means its actual PC died, so it must NOT be silently revived here.
|
|
161
|
+
// Only when the peer is no longer in an active call, so we never wipe a leg
|
|
162
|
+
// whose end-call reneg is still in flight.
|
|
163
|
+
_recoverFailedLegs() {
|
|
164
|
+
for (const ref of ["a", "b"]) {
|
|
165
|
+
const leg = this.legs[ref];
|
|
166
|
+
const peer = this.legs[ref === "a" ? "b" : "a"];
|
|
167
|
+
if (leg.kind === "sip" && leg.state === LEG_STATES.FAILED && !isActiveCall(peer.state)) {
|
|
168
|
+
leg.setState(LEG_STATES.DISCONNECTED, { reason: "failed-leg-recovery", from: "self" });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
136
173
|
async _executeMedia(action) {
|
|
137
174
|
try {
|
|
138
175
|
if (action.op === "connect") {
|
|
@@ -182,6 +182,40 @@ test("webrtc<->sip: remote BYE on sip leg ends the webrtc caller", async () => {
|
|
|
182
182
|
assert.equal(a.leg.state, S.CONNECTED);
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
+
test("sip ring failure ends the webrtc caller, then the sip leg recovers for reuse", async () => {
|
|
186
|
+
const a = makeWebRtcLeg("alice");
|
|
187
|
+
const b = makeSipLeg("+15551234");
|
|
188
|
+
const { poly, media } = buildPoly(a, b);
|
|
189
|
+
await poly.onIngress("a", makeLegEvent(LEG_EVENTS.TRANSPORT_OPEN));
|
|
190
|
+
await poly.onIngress("b", makeLegEvent(LEG_EVENTS.TRANSPORT_OPEN));
|
|
191
|
+
|
|
192
|
+
// The SIP INVITE is rejected before it establishes -> ring() rejects.
|
|
193
|
+
const realRing = b.negotiation.ring.bind(b.negotiation);
|
|
194
|
+
b.negotiation.ring = async () => { throw new Error("SIP call terminated before established"); };
|
|
195
|
+
|
|
196
|
+
// Caller offers -> P rings the sip leg, which fails -> sip FAILED -> P ends the
|
|
197
|
+
// caller via an end-call reneg (no media ever bridged).
|
|
198
|
+
await poly.onIngress("a", makeLegEvent(LEG_EVENTS.OFFER, { sdp: "o" }));
|
|
199
|
+
assert.equal(b.leg.state, S.FAILED, "sip leg failed its ring");
|
|
200
|
+
assert.equal(a.leg.state, S.ENDING, "caller is ended on the failed ring");
|
|
201
|
+
assert.equal(a.negotiation.named("endCall").length, 1);
|
|
202
|
+
assert.equal(media.connects.length, 0, "no media bridged for a failed ring");
|
|
203
|
+
|
|
204
|
+
// Caller's client answers the end-call offer -> caller back to connected, and
|
|
205
|
+
// the stuck FAILED sip leg recovers to its idle (DISCONNECTED) so it is reusable.
|
|
206
|
+
await poly.onIngress("a", makeLegEvent(LEG_EVENTS.END_RENEGOTIATION, { type: "answer", sdp: "end-ans" }));
|
|
207
|
+
assert.equal(a.leg.state, S.CONNECTED);
|
|
208
|
+
assert.equal(b.leg.state, S.DISCONNECTED, "failed sip leg recovered to idle for reuse");
|
|
209
|
+
|
|
210
|
+
// Reuse: sip is reachable again -> a fresh offer connects + rings the sip leg
|
|
211
|
+
// (a blocking INVITE that answers -> both in-call, media bridged).
|
|
212
|
+
b.negotiation.ring = realRing;
|
|
213
|
+
await poly.onIngress("a", makeLegEvent(LEG_EVENTS.OFFER, { sdp: "o2" }));
|
|
214
|
+
assert.equal(a.leg.state, S.IN_CALL);
|
|
215
|
+
assert.equal(b.leg.state, S.IN_CALL, "sip leg rung again on reuse");
|
|
216
|
+
assert.equal(media.connects.length, 1, "media bridged on the reused call");
|
|
217
|
+
});
|
|
218
|
+
|
|
185
219
|
test("reject before answer: caller is ended, no media ever bridged", async () => {
|
|
186
220
|
const a = makeWebRtcLeg("alice");
|
|
187
221
|
const b = makeWebRtcLeg("bob");
|
|
@@ -27,6 +27,12 @@ test("teardown beats progress: inCall vs failed (dropped) ends the inCall side,
|
|
|
27
27
|
assert.deepEqual(ends[0], { kind: "intent", leg: "a", intent: I.END, from: "b" });
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
test("calling vs failed (e.g. sip ring rejected) ends the calling caller", () => {
|
|
31
|
+
const actions = reconcile(snap(S.CALLING, S.FAILED, false));
|
|
32
|
+
assert.deepEqual(mediaOps(actions), []);
|
|
33
|
+
assert.deepEqual(intents(actions), [{ kind: "intent", leg: "a", intent: I.END, from: "b" }]);
|
|
34
|
+
});
|
|
35
|
+
|
|
30
36
|
test("inCall vs disconnected (peer gone, e.g. sip BYE) ends the inCall side, stops media", () => {
|
|
31
37
|
// You cannot be in a call by yourself: a disconnected peer ends the inCall leg.
|
|
32
38
|
const actions = reconcile(snap(S.IN_CALL, S.DISCONNECTED, true));
|
|
@@ -39,14 +39,14 @@ function createPeerConnectionFactory({
|
|
|
39
39
|
s.connectionState = state;
|
|
40
40
|
session.connectionState = state;
|
|
41
41
|
if (shouldRequestDestroy && (state === "failed" || state === "closed")) {
|
|
42
|
-
onSessionDestroyRequested(sessionId, { source: "webrtc", reason: `peer-connection-${state}`, notify: true });
|
|
42
|
+
onSessionDestroyRequested(sessionId, { source: "webrtc", reason: `peer-connection-${state}`, notify: true, pc });
|
|
43
43
|
} else if (state === "disconnected") {
|
|
44
44
|
if (!s.disconnectTimer) {
|
|
45
45
|
s.disconnectTimer = setTimeout(() => {
|
|
46
46
|
s.disconnectTimer = null;
|
|
47
47
|
const current = sessions.get(sessionId);
|
|
48
48
|
if (shouldRequestDestroy && current && session.peerConnection === pc && session.connectionState === "disconnected") {
|
|
49
|
-
onSessionDestroyRequested(sessionId, { source: "webrtc", reason: "peer-connection-disconnected", notify: true });
|
|
49
|
+
onSessionDestroyRequested(sessionId, { source: "webrtc", reason: "peer-connection-disconnected", notify: true, pc });
|
|
50
50
|
}
|
|
51
51
|
}, 5000);
|
|
52
52
|
}
|
|
@@ -1166,6 +1166,15 @@ function isSessionInCall(session) {
|
|
|
1166
1166
|
// Terminal PC state -> tear the pair down and clean up.
|
|
1167
1167
|
async function onTransportClosed(sessionId, event = {}) {
|
|
1168
1168
|
const session = sessions.get(sessionId);
|
|
1169
|
+
// A 2nd call reuses the same sessionId on a fresh transport. The old call's PC
|
|
1170
|
+
// can fire `closed` after the new session is already bound under this id. If the
|
|
1171
|
+
// closing transport is no longer the session's current caller/callee PC, it is
|
|
1172
|
+
// superseded -> ignore it entirely so we don't tear down the fresh session.
|
|
1173
|
+
if (session && event.pc &&
|
|
1174
|
+
event.pc !== session.peerConnection &&
|
|
1175
|
+
event.pc !== session.outboundWebrtc?.peerConnection) {
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1169
1178
|
const poly = polyForSession(session);
|
|
1170
1179
|
// Only tear the poly down if it actually owns this closing transport. A stale
|
|
1171
1180
|
// PC closing after a new-ring rebuild resolves (by identity) to the freshly
|