arnacon-webrtc-service 0.1.71 → 0.1.73
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
|
@@ -98,14 +98,16 @@ function createBridgeApi({
|
|
|
98
98
|
return trimmed;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function
|
|
101
|
+
function narrowAudioOfferToPayloads(sdp, allowedPayloads) {
|
|
102
102
|
if (!sdp || !sdp.includes("m=audio")) return sdp;
|
|
103
|
+
const allowedPayloadSet = new Set((allowedPayloads || []).map(String));
|
|
104
|
+
if (allowedPayloadSet.size === 0) return sdp;
|
|
103
105
|
const sections = sdp.split(/\r\n(?=m=)/);
|
|
104
106
|
return sections.map((section) => {
|
|
105
107
|
if (!section.startsWith("m=audio")) return section;
|
|
106
108
|
const lines = section.split(/\r\n/);
|
|
107
109
|
const mLine = lines[0].split(" ");
|
|
108
|
-
const payloads = mLine.slice(3).filter((pt) => pt
|
|
110
|
+
const payloads = mLine.slice(3).filter((pt) => allowedPayloadSet.has(pt));
|
|
109
111
|
if (!payloads.length) return section;
|
|
110
112
|
|
|
111
113
|
const allowed = new Set(payloads);
|
|
@@ -119,6 +121,13 @@ function createBridgeApi({
|
|
|
119
121
|
}).join("\r\n");
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
function narrowAudioOfferForCodecPolicy(sdp, codecPolicy) {
|
|
125
|
+
if (codecPolicy === "pcmu") return narrowAudioOfferToPayloads(sdp, ["0"]);
|
|
126
|
+
if (codecPolicy === "pcma") return narrowAudioOfferToPayloads(sdp, ["8"]);
|
|
127
|
+
if (codecPolicy === "g711") return narrowAudioOfferToPayloads(sdp, ["0", "8"]);
|
|
128
|
+
return sdp;
|
|
129
|
+
}
|
|
130
|
+
|
|
122
131
|
function attachOutboundDataChannel(legSessionId, legSession) {
|
|
123
132
|
const pc = createPeerConnection(legSessionId);
|
|
124
133
|
if (typeof pc.createDataChannel !== "function") return pc;
|
|
@@ -186,11 +195,11 @@ function createBridgeApi({
|
|
|
186
195
|
const candidatesToEmbed = srflxAndRelay.length > 0 ? srflxAndRelay : gatheredCandidates;
|
|
187
196
|
const relayCandidates = getRelayCandidates(gatheredCandidates);
|
|
188
197
|
let baseOfferSdp = offer.sdp;
|
|
189
|
-
if (callerSession.mediaCodecPolicy
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
logger.log(`[${legSessionId}] WebRTC leg inherited
|
|
193
|
-
baseOfferSdp =
|
|
198
|
+
if (callerSession.mediaCodecPolicy) {
|
|
199
|
+
const narrowedOfferSdp = narrowAudioOfferForCodecPolicy(baseOfferSdp, callerSession.mediaCodecPolicy);
|
|
200
|
+
if (narrowedOfferSdp !== baseOfferSdp) {
|
|
201
|
+
logger.log(`[${legSessionId}] WebRTC leg inherited bridge codec policy=${callerSession.mediaCodecPolicy}`);
|
|
202
|
+
baseOfferSdp = narrowedOfferSdp;
|
|
194
203
|
}
|
|
195
204
|
}
|
|
196
205
|
const offerSdp = embedCandidatesInSdp(baseOfferSdp, candidatesToEmbed);
|
|
@@ -100,11 +100,11 @@ function createCallFlowApi({
|
|
|
100
100
|
const candidatesToEmbed = srflxAndRelay.length > 0 ? srflxAndRelay : gatheredCandidates;
|
|
101
101
|
const relayCandidates = getRelayCandidates(gatheredCandidates);
|
|
102
102
|
let baseOfferSdp = offer.sdp;
|
|
103
|
-
if (session.mediaCodecPolicy
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
106
|
-
logger.log(`[${sessionId}] outbound WebRTC leg: narrowed RING offer
|
|
107
|
-
baseOfferSdp =
|
|
103
|
+
if (session.mediaCodecPolicy) {
|
|
104
|
+
const narrowedOfferSdp = narrowAudioOfferForCodecPolicy(baseOfferSdp, session.mediaCodecPolicy);
|
|
105
|
+
if (narrowedOfferSdp !== baseOfferSdp) {
|
|
106
|
+
logger.log(`[${sessionId}] outbound WebRTC leg: narrowed RING offer codecPolicy=${session.mediaCodecPolicy}`);
|
|
107
|
+
baseOfferSdp = narrowedOfferSdp;
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
const offerSdp = embedCandidatesInSdp(baseOfferSdp, candidatesToEmbed);
|
|
@@ -198,6 +198,30 @@ function createCallFlowApi({
|
|
|
198
198
|
return narrowAudioOfferToPayloads(sdp, ["8"]);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
function narrowAudioOfferToPcmu(sdp) {
|
|
202
|
+
return narrowAudioOfferToPayloads(sdp, ["0"]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function narrowAudioOfferForCodecPolicy(sdp, codecPolicy) {
|
|
206
|
+
if (codecPolicy === "pcma") return narrowAudioOfferToPcma(sdp);
|
|
207
|
+
if (codecPolicy === "pcmu") return narrowAudioOfferToPcmu(sdp);
|
|
208
|
+
if (codecPolicy === "g711") return narrowAudioOfferToG711(sdp);
|
|
209
|
+
return sdp;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getPrimaryAudioPayload(sdp) {
|
|
213
|
+
const audioSection = String(sdp || "").match(/m=audio[^\r\n]*[\s\S]*?(?=\r?\nm=|$)/m)?.[0] || "";
|
|
214
|
+
const mLine = audioSection.match(/^m=audio[^\r\n]*/m)?.[0] || "";
|
|
215
|
+
return mLine.split(/\s+/).slice(3)[0] || null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function exactG711PolicyFromAnswer(answerSdp) {
|
|
219
|
+
const primaryPt = getPrimaryAudioPayload(answerSdp);
|
|
220
|
+
if (primaryPt === "0") return "pcmu";
|
|
221
|
+
if (primaryPt === "8") return "pcma";
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
201
225
|
function routeCodecPolicy(destination, isInbound) {
|
|
202
226
|
if (isInbound) return false;
|
|
203
227
|
if (destination?.route === "sbc") return "pcma";
|
|
@@ -294,7 +318,14 @@ function createCallFlowApi({
|
|
|
294
318
|
ensureLocalAudioTrack(session, pc, sessionId);
|
|
295
319
|
const answerLabel = isInactive ? "PHASE 1 ANSWER SDP" : "ANSWER SDP";
|
|
296
320
|
const answerSdp = await createAnswerSdp(pc, sessionId, answerLabel);
|
|
297
|
-
if (!isInbound && destination?.route === "ivr")
|
|
321
|
+
if (!isInbound && destination?.route === "ivr") {
|
|
322
|
+
const exactPolicy = exactG711PolicyFromAnswer(answerSdp);
|
|
323
|
+
if (exactPolicy) {
|
|
324
|
+
session.mediaCodecPolicy = exactPolicy;
|
|
325
|
+
logger.log(`[${sessionId}] IVR bridge codec policy resolved to ${exactPolicy}`);
|
|
326
|
+
}
|
|
327
|
+
storeIvrNegotiatedAudio(session, sessionId, answerSdp);
|
|
328
|
+
}
|
|
298
329
|
|
|
299
330
|
if (!isInbound) sendAck(sessionId);
|
|
300
331
|
let routeResult = null;
|
|
@@ -2,14 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const DOMAINS = ["secnumtest.global", "secnum.global", "cellactm.global", "cellactl.global"];
|
|
4
4
|
const IVR_WAITING_AUDIO_FILE = "waiting.mp3";
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
callerSbcNumber: "972557258108",
|
|
8
|
-
ringTargets: [
|
|
9
|
-
"972557012402.secnumtest.global",
|
|
10
|
-
"972557012403.secnumtest.global",
|
|
11
|
-
],
|
|
12
|
-
};
|
|
5
|
+
const MULTIRING_CONFIG_BASE_URL = "https://lightpbx-save-config-343948402138.europe-west1.run.app";
|
|
6
|
+
const MULTIRING_CONFIG_TIMEOUT_MS = 2500;
|
|
13
7
|
|
|
14
8
|
function getDomains(helpers) {
|
|
15
9
|
const configured = helpers.getServiceConstants()?.domains;
|
|
@@ -45,17 +39,94 @@ async function resolveEnsWallet(helpers, ensName) {
|
|
|
45
39
|
return null;
|
|
46
40
|
}
|
|
47
41
|
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
42
|
+
function normalizeMultiringEndpoint(value, helpers) {
|
|
43
|
+
const firstLabel = String(value || "").trim().split(".")[0] || "";
|
|
44
|
+
return helpers.normalizePhone(firstLabel).replace(/\D/g, "");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function fetchMultiringConfig(endpoint, helpers) {
|
|
48
|
+
if (!endpoint || typeof fetch !== "function") return null;
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
const timeout = setTimeout(() => controller.abort(), MULTIRING_CONFIG_TIMEOUT_MS);
|
|
51
|
+
try {
|
|
52
|
+
const url = `${MULTIRING_CONFIG_BASE_URL}/get_mutiring/${encodeURIComponent(endpoint)}`;
|
|
53
|
+
const resp = await fetch(url, { method: "GET", signal: controller.signal });
|
|
54
|
+
if (!resp.ok) {
|
|
55
|
+
helpers.logRouteDecision?.({
|
|
56
|
+
serviceId: "secnum",
|
|
57
|
+
route: "multiring-config",
|
|
58
|
+
endpoint,
|
|
59
|
+
status: resp.status,
|
|
60
|
+
});
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return await resp.json();
|
|
64
|
+
} catch (err) {
|
|
65
|
+
helpers.logRouteDecision?.({
|
|
66
|
+
serviceId: "secnum",
|
|
67
|
+
route: "multiring-config",
|
|
68
|
+
endpoint,
|
|
69
|
+
error: err.message,
|
|
70
|
+
});
|
|
54
71
|
return null;
|
|
72
|
+
} finally {
|
|
73
|
+
clearTimeout(timeout);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function extractMultiringEmails(config) {
|
|
78
|
+
if (!config || config.ok === false || config.found === false) return [];
|
|
79
|
+
if (config?.route?.type !== "multi_ring") return [];
|
|
80
|
+
|
|
81
|
+
const directEmails = config?.route?.multiRing?.group?.selectedEmails;
|
|
82
|
+
if (Array.isArray(directEmails)) return directEmails;
|
|
83
|
+
|
|
84
|
+
const groupId = config?.route?.multiRing?.groupId;
|
|
85
|
+
if (!groupId || !Array.isArray(config?.groups)) return [];
|
|
86
|
+
const group = config.groups.find((item) => item?.id === groupId);
|
|
87
|
+
return Array.isArray(group?.selectedEmails) ? group.selectedEmails : [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function multiringTargetToEnsName(target, helpers, targetDomain) {
|
|
91
|
+
const value = String(target || "").trim().toLowerCase();
|
|
92
|
+
if (!value) return null;
|
|
93
|
+
|
|
94
|
+
if (value.includes("@") && !value.endsWith(".global")) {
|
|
95
|
+
return helpers.emailToEnsName(helpers.normalizeEmail(value), targetDomain);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (value.endsWith(".global")) {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const normalizedPhone = helpers.normalizePhone(value).replace(/\D/g, "");
|
|
103
|
+
if (normalizedPhone) {
|
|
104
|
+
return `${normalizedPhone}.${targetDomain}`;
|
|
55
105
|
}
|
|
56
106
|
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function buildConfiguredMultiRing(parsedTo, helpers) {
|
|
111
|
+
const rawTarget = helpers.normalizePhone(parsedTo?.value || parsedTo?.full || "");
|
|
112
|
+
if (!/^\d+$/.test(rawTarget)) return null;
|
|
113
|
+
|
|
114
|
+
const endpoint = normalizeMultiringEndpoint(rawTarget, helpers);
|
|
115
|
+
if (!endpoint) return null;
|
|
116
|
+
|
|
117
|
+
const config = await fetchMultiringConfig(endpoint, helpers);
|
|
118
|
+
const ringTargets = extractMultiringEmails(config)
|
|
119
|
+
.map((target) => String(target || "").trim())
|
|
120
|
+
.filter(Boolean);
|
|
121
|
+
if (ringTargets.length === 0) return null;
|
|
122
|
+
|
|
123
|
+
const targetDomain = getDomains(helpers)[0];
|
|
57
124
|
const targets = [];
|
|
58
|
-
|
|
125
|
+
const seenEnsNames = new Set();
|
|
126
|
+
for (const ringTarget of ringTargets) {
|
|
127
|
+
const ensName = multiringTargetToEnsName(ringTarget, helpers, targetDomain);
|
|
128
|
+
if (!ensName || seenEnsNames.has(ensName)) continue;
|
|
129
|
+
seenEnsNames.add(ensName);
|
|
59
130
|
const wallet = await resolveEnsWallet(helpers, ensName);
|
|
60
131
|
if (!wallet) continue;
|
|
61
132
|
targets.push({ wallet, ensName });
|
|
@@ -69,7 +140,7 @@ async function buildHardcodedMultiRing(parsedFrom, helpers) {
|
|
|
69
140
|
route: "webrtc-multiring",
|
|
70
141
|
mode: "first-verified-answer-wins",
|
|
71
142
|
targets,
|
|
72
|
-
ruleId:
|
|
143
|
+
ruleId: `gcp-secnum-multiring:${endpoint}`,
|
|
73
144
|
};
|
|
74
145
|
}
|
|
75
146
|
|
|
@@ -99,9 +170,6 @@ async function resolveDestination(ctx) {
|
|
|
99
170
|
const { parsedTo, parsedFrom, helpers } = ctx;
|
|
100
171
|
if (!parsedTo) return { route: "reject", reason: "Missing destination" };
|
|
101
172
|
|
|
102
|
-
const multiRing = await buildHardcodedMultiRing(parsedFrom, helpers);
|
|
103
|
-
if (multiRing) return multiRing;
|
|
104
|
-
|
|
105
173
|
const normalizedTarget = helpers.normalizePhone(parsedTo.value || parsedTo.full || "");
|
|
106
174
|
if (normalizedTarget === "2006") {
|
|
107
175
|
return {
|
|
@@ -112,6 +180,9 @@ async function resolveDestination(ctx) {
|
|
|
112
180
|
};
|
|
113
181
|
}
|
|
114
182
|
|
|
183
|
+
const multiRing = await buildConfiguredMultiRing(parsedTo, helpers);
|
|
184
|
+
if (multiRing) return multiRing;
|
|
185
|
+
|
|
115
186
|
if (parsedTo.type === "raw" || parsedTo.type === "unknown") {
|
|
116
187
|
return { route: "sbc", number: helpers.normalizePhone(parsedTo.value) };
|
|
117
188
|
}
|