@cemscale-voip/voip-sdk 2.0.16 → 2.0.17
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/README.md +203 -331
- package/dist/client.d.ts +19 -213
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +24 -219
- package/dist/client.js.map +1 -1
- package/dist/hooks/useVoIP.d.ts +9 -7
- package/dist/hooks/useVoIP.d.ts.map +1 -1
- package/dist/hooks/useVoIP.js +95 -57
- package/dist/hooks/useVoIP.js.map +1 -1
- package/dist/types.d.ts +27 -211
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,12 +43,13 @@ await voip.transfer(callUuid, { targetExtension: '1002', type: 'blind' });
|
|
|
43
43
|
await voip.hangup(callUuid);
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
### Three-Way Calling (Conferencia Tripartita) —
|
|
46
|
+
### Three-Way Calling (Conferencia Tripartita) — Conference-Based, 1-Step
|
|
47
47
|
|
|
48
|
-
>
|
|
49
|
-
>
|
|
50
|
-
>
|
|
51
|
-
>
|
|
48
|
+
> The three-way calling system uses **FreeSWITCH conferences**. When you add a
|
|
49
|
+
> participant, both call legs are transferred into a conference immediately.
|
|
50
|
+
> Person A is put on hold (muted + deaf + hears MOH). Person B rings directly
|
|
51
|
+
> into the conference. **No separate merge step is needed** — the conference is
|
|
52
|
+
> active from the moment you call `addParticipant()`.
|
|
52
53
|
|
|
53
54
|
---
|
|
54
55
|
|
|
@@ -57,173 +58,143 @@ await voip.hangup(callUuid);
|
|
|
57
58
|
```
|
|
58
59
|
PASO 0: El agente está en llamada con Persona A. currentCall.state === 'established'.
|
|
59
60
|
PASO 1: El agente presiona "Agregar" → addParticipant('numero de Persona B').
|
|
60
|
-
La API
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
La API transfiere ambas patas a conferencia, pone A en hold (MOH),
|
|
62
|
+
y origina llamada a B directamente dentro de la conferencia.
|
|
63
|
+
threeWaySession.state → 'dialing' ← MOSTRAR "Llamando a Persona B..."
|
|
64
|
+
PASO 2: Persona B contesta → entra automáticamente a la conferencia.
|
|
65
|
+
El SDK detecta via polling → threeWaySession.state → 'holding'
|
|
66
|
+
El agente habla con B, A sigue en hold (MOH).
|
|
67
|
+
PASO 3a: SWAP — El agente quiere hablar con A en privado.
|
|
68
|
+
swapParticipant(keepUuid=A, holdUuid=B) → B va a hold (MOH), A vuelve.
|
|
69
|
+
PASO 3b: MERGE — El agente presiona "Conferencia" (todos hablan).
|
|
70
|
+
mergeThreeWay() → Todos salen de hold, los 3 se escuchan.
|
|
71
|
+
PASO 3c: CANCEL — Persona B no contestó.
|
|
72
|
+
cancelAddParticipant() → Mata a B, reanuda a A, conferencia 2 personas.
|
|
73
|
+
PASO 4: KICK — Sacar a alguien específico.
|
|
74
|
+
dropParticipant(memberUuid) → Esa persona sale de la conferencia.
|
|
67
75
|
```
|
|
68
76
|
|
|
69
77
|
---
|
|
70
78
|
|
|
71
|
-
#### FLOW 1 — `
|
|
72
|
-
|
|
73
|
-
Este es el flujo con el hook `useVoIP`. **El hook maneja TODO automáticamente:**
|
|
74
|
-
resolución de UUID de FreeSWITCH, estados de la tripartita, y limpieza al colgar.
|
|
79
|
+
#### FLOW 1 — `useVoIP` hook (WebRTC softphone, recomendado)
|
|
75
80
|
|
|
76
81
|
##### 1.1 — Configuración inicial
|
|
77
82
|
|
|
78
83
|
```typescript
|
|
79
84
|
import { useVoIP } from '@cemscale-voip/voip-sdk';
|
|
80
85
|
|
|
81
|
-
// Dentro de tu componente React:
|
|
82
86
|
const {
|
|
83
|
-
// Estado del teléfono
|
|
84
87
|
isRegistered, // boolean — SIP registrado?
|
|
85
|
-
currentCall, // WebRTCCallInfo | null —
|
|
86
|
-
error, // string | null — último error
|
|
87
|
-
|
|
88
|
-
// Acciones de llamada básica
|
|
88
|
+
currentCall, // WebRTCCallInfo | null — llamada activa
|
|
89
|
+
error, // string | null — último error
|
|
89
90
|
startPhone, // (ext, password, displayName?) => Promise<void>
|
|
90
91
|
call, // (number) => Promise<WebRTCCallInfo>
|
|
91
|
-
answer, // () => Promise<void>
|
|
92
92
|
hangup, // () => Promise<void>
|
|
93
|
-
toggleHold, // () => Promise<boolean>
|
|
94
93
|
toggleMute, // () => boolean
|
|
95
94
|
|
|
96
|
-
//
|
|
95
|
+
// Three-way call
|
|
97
96
|
addParticipant, // (target: string) => Promise<void>
|
|
98
|
-
mergeThreeWay, // () => Promise<void>
|
|
99
|
-
swapParticipant, // (keepUuid
|
|
100
|
-
dropParticipant, // (
|
|
97
|
+
mergeThreeWay, // () => Promise<void> — unmute/undeaf todos
|
|
98
|
+
swapParticipant, // (keepUuid, holdUuid) => Promise<void>
|
|
99
|
+
dropParticipant, // (memberUuid: string) => Promise<void>
|
|
100
|
+
cancelAddParticipant, // () => Promise<void> — B no contestó
|
|
101
|
+
endThreeWay, // () => Promise<void>
|
|
101
102
|
|
|
102
|
-
// Estado de la tripartita
|
|
103
103
|
threeWaySession, // ThreeWaySession | null
|
|
104
104
|
} = useVoIP({
|
|
105
105
|
apiUrl: 'https://voip-api.cemscale.com',
|
|
106
|
-
apiKey: 'csk_live_...',
|
|
106
|
+
apiKey: 'csk_live_...',
|
|
107
107
|
});
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
##### 1.2 — `ThreeWaySession`
|
|
111
|
-
|
|
112
|
-
Este objeto describe TODO el estado de la tripartita. Tu UI debe leerlo para decidir qué mostrar.
|
|
110
|
+
##### 1.2 — `ThreeWaySession` y `ThreeWayParticipant`
|
|
113
111
|
|
|
114
112
|
```typescript
|
|
115
113
|
interface ThreeWaySession {
|
|
116
|
-
conferenceName: string; //
|
|
117
|
-
state: '
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
pendingUuid: string | undefined; // UUID de Persona B. Existe en state='adding'
|
|
123
|
-
originalBridgedLeg: string | undefined; // UUID de la pata PSTN de Persona A
|
|
114
|
+
conferenceName: string; // "3way_<tenantId>_<random>"
|
|
115
|
+
state: 'dialing' | 'holding' | 'active' | 'swapping';
|
|
116
|
+
participants: ThreeWayParticipant[];
|
|
117
|
+
pendingUuid?: string; // UUID de Persona B mientras suena
|
|
118
|
+
heldMemberUuid?: string; // UUID del participante en hold
|
|
119
|
+
originalBridgedLeg?: string;
|
|
124
120
|
}
|
|
125
121
|
|
|
126
122
|
interface ThreeWayParticipant {
|
|
127
|
-
uuid: string;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
##### 1.3 — `WebRTCCallInfo` (campos relevantes para tripartita)
|
|
136
|
-
|
|
137
|
-
```typescript
|
|
138
|
-
interface WebRTCCallInfo {
|
|
139
|
-
id: string; // ID local de SIP.js — ** NO USAR para API calls **
|
|
140
|
-
fsChannelUuid: string | null; // UUID de FreeSWITCH — ** USAR ESTE para API calls **
|
|
141
|
-
// null mientras la llamada está conectando
|
|
142
|
-
// poblado automáticamente cuando state === 'established'
|
|
143
|
-
state: string; // 'ringing' | 'connecting' | 'established' | 'held' | 'terminated' | ...
|
|
144
|
-
direction: 'inbound' | 'outbound';
|
|
145
|
-
remoteIdentity: string; // Número remoto (ej: '+17866302522')
|
|
146
|
-
remoteDisplayName: string; // Nombre mostrado
|
|
147
|
-
held: boolean;
|
|
148
|
-
muted: boolean;
|
|
123
|
+
uuid: string; // UUID del canal FreeSWITCH
|
|
124
|
+
memberId: string; // ID de miembro en la conferencia
|
|
125
|
+
callerIdNumber: string; // "+17865551234" o "1002"
|
|
126
|
+
callerIdName: string; // "John Smith"
|
|
127
|
+
muted: boolean; // true si está muteado
|
|
128
|
+
deaf: boolean; // true si está ensordecido
|
|
129
|
+
status: 'active' | 'held'; // 'held' = muted + deaf + MOH
|
|
149
130
|
}
|
|
150
131
|
```
|
|
151
132
|
|
|
152
|
-
##### 1.
|
|
133
|
+
##### 1.3 — Flujo COMPLETO paso a paso
|
|
153
134
|
|
|
154
135
|
```typescript
|
|
155
136
|
// ═══════════════════════════════════════════════════════════
|
|
156
137
|
// PASO 0: Agente en llamada con Persona A
|
|
157
138
|
// ═══════════════════════════════════════════════════════════
|
|
158
|
-
|
|
159
|
-
//
|
|
160
|
-
// currentCall.fsChannelUuid !== null ← resuelto automáticamente
|
|
161
|
-
// threeWaySession === null ← no hay tripartita activa
|
|
139
|
+
// currentCall.state === 'established' ← condición para botón "Agregar"
|
|
140
|
+
// threeWaySession === null
|
|
162
141
|
|
|
163
142
|
// ═══════════════════════════════════════════════════════════
|
|
164
143
|
// PASO 1: Agregar Persona B
|
|
165
144
|
// ═══════════════════════════════════════════════════════════
|
|
145
|
+
await addParticipant('+17866302522');
|
|
146
|
+
// La API:
|
|
147
|
+
// 1. Transfiere WebRTC + Persona A a conferencia (uuid_transfer -both)
|
|
148
|
+
// 2. Mutea + ensordece a Persona A + le pone MOH (uuid_displace)
|
|
149
|
+
// 3. Origina llamada a Persona B directamente a la conferencia
|
|
150
|
+
// 4. Retorna inmediatamente
|
|
151
|
+
|
|
152
|
+
// DESPUÉS:
|
|
153
|
+
// threeWaySession.state === 'dialing' ← Persona B está sonando
|
|
154
|
+
// threeWaySession.conferenceName !== '' ← Conferencia YA existe
|
|
155
|
+
// ❌ NO iniciar timer — B no ha contestado
|
|
166
156
|
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// 4. API origina llamada a Persona B vía trunk
|
|
176
|
-
// 5. Retorna { newCallUuid, originalBridgedLeg }
|
|
177
|
-
// 6. SDK setea threeWaySession = { state: 'adding', pendingUuid, ... }
|
|
178
|
-
|
|
179
|
-
// ⚠️ DESPUÉS DE ESTA LLAMADA:
|
|
180
|
-
// threeWaySession.state === 'adding' ← MOSTRAR "Llamando a Persona B..."
|
|
181
|
-
// threeWaySession.pendingUuid !== undefined ← Persona B está sonando
|
|
182
|
-
// currentCall aún existe (Persona A en hold)
|
|
183
|
-
// ❌ NO iniciar timer — Persona B NO ha contestado todavía
|
|
184
|
-
// ❌ NO llamar a mergeThreeWay() todavía — Persona B no ha contestado
|
|
185
|
-
|
|
186
|
-
} catch (err) {
|
|
187
|
-
// addParticipant falló. Persona A vuelve de hold automáticamente.
|
|
188
|
-
console.error('Error al agregar participante:', err);
|
|
189
|
-
}
|
|
157
|
+
// ═══════════════════════════════════════════════════════════
|
|
158
|
+
// PASO 2: Persona B contesta (detectado por polling automático)
|
|
159
|
+
// ═══════════════════════════════════════════════════════════
|
|
160
|
+
// El hook hace polling de getConferenceMembers() cada 2s.
|
|
161
|
+
// Cuando detecta que B entró a la conferencia:
|
|
162
|
+
// threeWaySession.state → 'holding'
|
|
163
|
+
// threeWaySession.participants → [agente(active), A(held), B(active)]
|
|
164
|
+
// El agente habla con B. A escucha MOH.
|
|
190
165
|
|
|
191
166
|
// ═══════════════════════════════════════════════════════════
|
|
192
|
-
// PASO
|
|
167
|
+
// PASO 3a: SWAP — Hablar con A, poner B en hold
|
|
193
168
|
// ═══════════════════════════════════════════════════════════
|
|
169
|
+
await swapParticipant(personA.uuid, personB.uuid);
|
|
170
|
+
// B → muted + deaf + MOH
|
|
171
|
+
// A → unmuted + undeaf + stop MOH
|
|
172
|
+
// threeWaySession.state → 'holding'
|
|
194
173
|
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
|
|
174
|
+
// ═══════════════════════════════════════════════════════════
|
|
175
|
+
// PASO 3b: MERGE — Todos hablan (conferencia tripartita)
|
|
176
|
+
// ═══════════════════════════════════════════════════════════
|
|
177
|
+
await mergeThreeWay();
|
|
178
|
+
// Desmutea + undeaf a TODOS los participantes en hold
|
|
179
|
+
// threeWaySession.state → 'active'
|
|
180
|
+
// ✅ AHORA sí iniciar timer de conferencia
|
|
199
181
|
|
|
200
182
|
// ═══════════════════════════════════════════════════════════
|
|
201
|
-
// PASO
|
|
183
|
+
// PASO 3c: CANCEL — B no contestó
|
|
202
184
|
// ═══════════════════════════════════════════════════════════
|
|
185
|
+
await cancelAddParticipant();
|
|
186
|
+
// Mata canal de B, desmutea + undeaf a A
|
|
187
|
+
// Conferencia sigue con 2 personas (agente + A)
|
|
203
188
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// 2. Toma threeWaySession.pendingUuid (callUuidB)
|
|
211
|
-
// 3. Toma threeWaySession.originalBridgedLeg
|
|
212
|
-
// 4. Llama POST /api/calls/three-way/merge
|
|
213
|
-
// 5. API mueve todos a conferencia
|
|
214
|
-
// 6. SDK setea threeWaySession = { state: 'active', participants: [...] }
|
|
215
|
-
|
|
216
|
-
// ✅ DESPUÉS DE ESTA LLAMADA:
|
|
217
|
-
// threeWaySession.state === 'active' ← AHORA sí iniciar timer de conferencia
|
|
218
|
-
// threeWaySession.participants.length >= 2 ← Persona A + Persona B (más el agente)
|
|
219
|
-
// currentCall sigue activo (el agente está en la conferencia)
|
|
220
|
-
|
|
221
|
-
} catch (err) {
|
|
222
|
-
console.error('Error al fusionar:', err);
|
|
223
|
-
}
|
|
189
|
+
// ═══════════════════════════════════════════════════════════
|
|
190
|
+
// PASO 4: KICK — Sacar a alguien de la conferencia
|
|
191
|
+
// ═══════════════════════════════════════════════════════════
|
|
192
|
+
await dropParticipant(personB.uuid);
|
|
193
|
+
// B sale de la conferencia (su canal cuelga)
|
|
194
|
+
// Si solo queda 1 participante, threeWaySession → null
|
|
224
195
|
```
|
|
225
196
|
|
|
226
|
-
##### 1.
|
|
197
|
+
##### 1.4 — Qué mostrar en la UI en CADA estado
|
|
227
198
|
|
|
228
199
|
```typescript
|
|
229
200
|
// ── SIN LLAMADA ──
|
|
@@ -238,281 +209,182 @@ if (currentCall && !threeWaySession) {
|
|
|
238
209
|
<CallTimer startTime={currentCall.answerTime} />
|
|
239
210
|
<RemoteParty name={currentCall.remoteDisplayName} number={currentCall.remoteIdentity} />
|
|
240
211
|
<MuteButton active={currentCall.muted} onPress={toggleMute} />
|
|
241
|
-
<HoldButton active={currentCall.held} onPress={toggleHold} />
|
|
242
212
|
<HangupButton onPress={hangup} />
|
|
243
213
|
<AddParticipantButton onPress={() => showAddParticipantInput()} />
|
|
244
|
-
{/* ↑ Solo mostrar si currentCall.state === 'established' */}
|
|
245
214
|
</InCallScreen>
|
|
246
215
|
);
|
|
247
216
|
}
|
|
248
217
|
|
|
249
|
-
// ──
|
|
250
|
-
if (threeWaySession?.state === '
|
|
218
|
+
// ── DIALING — Persona B está sonando ──
|
|
219
|
+
if (threeWaySession?.state === 'dialing') {
|
|
251
220
|
return (
|
|
252
221
|
<InCallScreen>
|
|
253
|
-
{/* Persona A sigue en hold — mostrar timer de Persona A */}
|
|
254
|
-
<CallTimer startTime={currentCall.answerTime} />
|
|
255
|
-
<RemoteParty name={currentCall.remoteDisplayName} number={currentCall.remoteIdentity} />
|
|
256
|
-
<HoldIndicator /> {/* Persona A está en hold */}
|
|
257
|
-
|
|
258
|
-
{/* Persona B */}
|
|
259
222
|
<CallingBanner>
|
|
260
223
|
<Spinner />
|
|
261
224
|
<Text>Llamando a Persona B...</Text>
|
|
262
|
-
{/* ❌ NO mostrar timer para Persona B — no ha contestado */}
|
|
263
225
|
</CallingBanner>
|
|
226
|
+
<CancelButton onPress={cancelAddParticipant} />
|
|
227
|
+
<HangupButton onPress={hangup} />
|
|
228
|
+
</InCallScreen>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
264
231
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
{
|
|
232
|
+
// ── HOLDING — Un participante en hold, otro activo ──
|
|
233
|
+
if (threeWaySession?.state === 'holding' || threeWaySession?.state === 'swapping') {
|
|
234
|
+
return (
|
|
235
|
+
<InCallScreen>
|
|
236
|
+
{threeWaySession.participants.map(p => (
|
|
237
|
+
<ParticipantRow key={p.uuid}>
|
|
238
|
+
<ParticipantName name={p.callerIdName} number={p.callerIdNumber} />
|
|
239
|
+
<ParticipantStatus status={p.status} /> {/* 'held' o 'active' */}
|
|
240
|
+
{p.status === 'held' && (
|
|
241
|
+
<UnholdButton onPress={() => {
|
|
242
|
+
const other = threeWaySession.participants.find(x => x.uuid !== p.uuid && x.status === 'active');
|
|
243
|
+
if (other) swapParticipant(p.uuid, other.uuid);
|
|
244
|
+
}} />
|
|
245
|
+
)}
|
|
246
|
+
</ParticipantRow>
|
|
247
|
+
))}
|
|
248
|
+
<MergeButton onPress={mergeThreeWay} label="Conferencia (3)" />
|
|
249
|
+
<HangupButton onPress={hangup} />
|
|
270
250
|
</InCallScreen>
|
|
271
251
|
);
|
|
272
252
|
}
|
|
273
253
|
|
|
274
|
-
// ──
|
|
254
|
+
// ── ACTIVE — Conferencia tripartita (todos hablan) ──
|
|
275
255
|
if (threeWaySession?.state === 'active') {
|
|
276
256
|
return (
|
|
277
257
|
<ConferenceScreen>
|
|
278
|
-
<
|
|
279
|
-
<ConferenceIcon />
|
|
280
|
-
<Text>Conferencia — 3 participantes</Text>
|
|
281
|
-
<ConferenceTimer startTime={conferenceStartTime} />
|
|
282
|
-
{/* ↑ AHORA sí iniciar timer */}
|
|
283
|
-
</ConferenceHeader>
|
|
284
|
-
|
|
258
|
+
<ConferenceTimer startTime={conferenceStartTime} />
|
|
285
259
|
{threeWaySession.participants.map(p => (
|
|
286
260
|
<ParticipantRow key={p.uuid}>
|
|
287
261
|
<ParticipantName name={p.callerIdName} number={p.callerIdNumber} />
|
|
288
|
-
<
|
|
289
|
-
{/* p.status === 'held' → mostrar "En espera" */}
|
|
290
|
-
<SwapButton onPress={() => swapParticipant(p.uuid, getOtherActiveUuid(p))} />
|
|
291
|
-
{/* ↑ Solo mostrar si hay >1 participante active */}
|
|
292
|
-
<DropButton onPress={() => dropParticipant(p.memberId)} />
|
|
262
|
+
<DropButton onPress={() => dropParticipant(p.uuid)} />
|
|
293
263
|
</ParticipantRow>
|
|
294
264
|
))}
|
|
295
|
-
|
|
296
|
-
<MuteButton active={currentCall.muted} onPress={toggleMute} />
|
|
297
265
|
<EndConferenceButton onPress={hangup} />
|
|
298
|
-
{/* ↑ Cuelga a TODOS y destruye la conferencia */}
|
|
299
266
|
</ConferenceScreen>
|
|
300
267
|
);
|
|
301
268
|
}
|
|
302
269
|
```
|
|
303
270
|
|
|
304
|
-
##### 1.
|
|
271
|
+
##### 1.5 — Errores comunes
|
|
305
272
|
|
|
306
273
|
| Error | Causa | Solución |
|
|
307
274
|
|-------|-------|----------|
|
|
308
|
-
| `"FreeSWITCH UUID not resolved yet"` | Llamaste `addParticipant` antes de que la llamada conecte | Espera
|
|
309
|
-
| `"No active call"` | `currentCall` es null | Verifica que haya una llamada activa
|
|
310
|
-
|
|
|
311
|
-
|
|
|
312
|
-
| La llamada se cuelga al hacer merge | Llamaste merge antes de que Persona B conteste | Asegúrate de que Persona B ya contestó antes de fusionar |
|
|
313
|
-
| `addParticipant` lanza error 500 | El backend no pudo originar a Persona B | Mostrar el mensaje de error al agente. La llamada con Persona A se reanuda sola. |
|
|
275
|
+
| `"FreeSWITCH UUID not resolved yet"` | Llamaste `addParticipant` antes de que la llamada conecte | Espera `currentCall.state === 'established'` y `fsChannelUuid !== null` |
|
|
276
|
+
| `"No active call"` | `currentCall` es null | Verifica que haya una llamada activa |
|
|
277
|
+
| Timer se inicia antes de contestar | Iniciaste timer en state='dialing' | Solo iniciar timer cuando `state === 'active'` |
|
|
278
|
+
| `addParticipant` lanza error 500 | El backend no pudo originar a Persona B | La llamada con A se reanuda automáticamente |
|
|
314
279
|
|
|
315
280
|
---
|
|
316
281
|
|
|
317
|
-
#### FLOW 2 —
|
|
318
|
-
|
|
319
|
-
Para cuando el agente usa un teléfono físico (Yealink, Grandstream) y manualmente:
|
|
320
|
-
1. Llama a Persona A
|
|
321
|
-
2. Pone en hold
|
|
322
|
-
3. Marca a Persona B
|
|
323
|
-
4. Quiere fusionar ambas
|
|
324
|
-
|
|
325
|
-
**NO usa `addParticipant`.** Solo detecta las 2 llamadas activas y las fusiona.
|
|
282
|
+
#### FLOW 2 — VoIPClient directo (sin hook)
|
|
326
283
|
|
|
327
284
|
```typescript
|
|
328
285
|
import { VoIPClient } from '@cemscale-voip/voip-sdk';
|
|
329
286
|
|
|
330
|
-
const voip = new VoIPClient({
|
|
331
|
-
|
|
332
|
-
|
|
287
|
+
const voip = new VoIPClient({ apiUrl: '...', apiKey: '...' });
|
|
288
|
+
|
|
289
|
+
// PASO 1: Agregar participante (conferencia se crea inmediatamente)
|
|
290
|
+
const result = await voip.addCallParticipant(fsUuid, {
|
|
291
|
+
toNumber: '+17866302522',
|
|
333
292
|
});
|
|
334
293
|
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
294
|
+
// result = {
|
|
295
|
+
// conferenceName: "3way_<tenantId>_<hex>",
|
|
296
|
+
// state: "dialing",
|
|
297
|
+
// participants: [
|
|
298
|
+
// { uuid: "webrtc-uuid", callerIdNumber: "1001", status: "active", ... },
|
|
299
|
+
// { uuid: "personA-uuid", callerIdNumber: "+1786...", status: "held", muted: true, deaf: true },
|
|
300
|
+
// ],
|
|
301
|
+
// newCallUuid: "personB-uuid",
|
|
302
|
+
// heldMemberUuid: "personA-uuid",
|
|
303
|
+
// originalBridgedLeg: "personA-uuid",
|
|
304
|
+
// }
|
|
338
305
|
|
|
339
|
-
//
|
|
306
|
+
// PASO 2: Detectar cuando B contesta (polling)
|
|
340
307
|
const pollInterval = setInterval(async () => {
|
|
341
|
-
const {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
c.caller_id_number === agentExt || c.destination === agentExt
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
// ¿Hay 2 o más llamadas activas?
|
|
350
|
-
if (myCalls.length >= 2) {
|
|
351
|
-
// Mostrar botón "Fusionar" con opción de seleccionar cuáles 2
|
|
352
|
-
showMergeButton(myCalls);
|
|
353
|
-
} else {
|
|
354
|
-
hideMergeButton();
|
|
355
|
-
}
|
|
356
|
-
}, 5000);
|
|
357
|
-
|
|
358
|
-
// ═══════════════════════════════════════════════════════
|
|
359
|
-
// PASO 2: Fusionar las 2 llamadas seleccionadas
|
|
360
|
-
// ═══════════════════════════════════════════════════════
|
|
361
|
-
|
|
362
|
-
async function handleMerge(callA: ActiveCall, callB: ActiveCall) {
|
|
363
|
-
try {
|
|
364
|
-
const result = await voip.mergeDirectCalls({
|
|
365
|
-
callUuidA: callA.uuid, // ⚠️ uuid de FreeSWITCH, NO call_uuid
|
|
366
|
-
callUuidB: callB.uuid,
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
console.log('Conferencia creada:', result.conferenceName);
|
|
370
|
-
// result.participants → Array con los 3 participantes
|
|
371
|
-
// La conferencia se destruye sola cuando el agente cuelga
|
|
372
|
-
|
|
373
|
-
} catch (err) {
|
|
374
|
-
console.error('Error al fusionar:', err);
|
|
308
|
+
const { conference } = await voip.getConferenceMembers(result.conferenceName);
|
|
309
|
+
const activeCount = conference.members.filter(m => !m.muted || !m.deaf).length;
|
|
310
|
+
if (activeCount >= 2) {
|
|
311
|
+
clearInterval(pollInterval);
|
|
312
|
+
// B contestó — ahora puedes swap/merge
|
|
375
313
|
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// ═══════════════════════════════════════════════════════
|
|
379
|
-
// LIMPIEZA
|
|
380
|
-
// ═══════════════════════════════════════════════════════
|
|
314
|
+
}, 2000);
|
|
381
315
|
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
```typescript
|
|
389
|
-
interface ActiveCall {
|
|
390
|
-
uuid: string; // UUID del canal FreeSWITCH — USAR ESTE
|
|
391
|
-
call_uuid: string; // UUID de la sesión de llamada (compartido entre A-leg y B-leg)
|
|
392
|
-
caller_id_number: string; // Quién llama (extensión o número externo)
|
|
393
|
-
caller_id_name: string; // Nombre del caller
|
|
394
|
-
destination: string; // Número marcado
|
|
395
|
-
direction: string; // 'inbound' | 'outbound'
|
|
396
|
-
callstate: string; // 'ACTIVE' | 'EARLY' | 'RINGING' | 'HANGUP'
|
|
397
|
-
answered: boolean;
|
|
398
|
-
on_hold: boolean;
|
|
399
|
-
}
|
|
400
|
-
```
|
|
316
|
+
// PASO 3a: Swap (hablar con A, poner B en hold)
|
|
317
|
+
await voip.swapCallParticipant({
|
|
318
|
+
conferenceName: result.conferenceName,
|
|
319
|
+
keepUuid: personA_uuid, // persona con quien hablar
|
|
320
|
+
holdUuid: personB_uuid, // persona que va a hold
|
|
321
|
+
});
|
|
401
322
|
|
|
402
|
-
|
|
323
|
+
// PASO 3b: Merge (todos hablan)
|
|
324
|
+
await voip.mergeThreeWay({
|
|
325
|
+
conferenceName: result.conferenceName,
|
|
326
|
+
});
|
|
403
327
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
328
|
+
// PASO 3c: Cancel (B no contestó)
|
|
329
|
+
await voip.cancelAddParticipant({
|
|
330
|
+
conferenceName: result.conferenceName,
|
|
331
|
+
newCallUuid: result.newCallUuid,
|
|
332
|
+
heldMemberUuid: result.heldMemberUuid,
|
|
333
|
+
});
|
|
407
334
|
|
|
408
|
-
|
|
409
|
-
|
|
335
|
+
// PASO 4: Kick (sacar a alguien)
|
|
336
|
+
await voip.kickParticipant({
|
|
337
|
+
conferenceName: result.conferenceName,
|
|
338
|
+
memberUuid: personB_uuid,
|
|
339
|
+
});
|
|
410
340
|
```
|
|
411
341
|
|
|
412
342
|
---
|
|
413
343
|
|
|
414
|
-
####
|
|
415
|
-
|
|
416
|
-
Si NO estás usando `useVoIP` (estás usando `VoIPClient` + `WebRTCPhone` manualmente):
|
|
417
|
-
|
|
418
|
-
```typescript
|
|
419
|
-
import { VoIPClient, WebRTCPhone } from '@cemscale-voip/voip-sdk';
|
|
420
|
-
|
|
421
|
-
const voip = new VoIPClient({ apiUrl: '...', apiKey: '...' });
|
|
422
|
-
const phone = new WebRTCPhone({ ... });
|
|
423
|
-
|
|
424
|
-
// ════════ PASO 1: Hacer llamada a Persona A ════════
|
|
425
|
-
const callInfo = await phone.call('+17863402240');
|
|
426
|
-
// Esperar: callInfo.state === 'established'
|
|
427
|
-
|
|
428
|
-
// ════════ PASO 2: Resolver el UUID de FreeSWITCH ════════
|
|
429
|
-
const fsUuid = await voip.resolveCallFsUuid(
|
|
430
|
-
callInfo.remoteIdentity.includes('100') ? callInfo.remoteIdentity : '1001', // extensión
|
|
431
|
-
callInfo.remoteIdentity, // número remoto
|
|
432
|
-
callInfo.direction // 'outbound' | 'inbound'
|
|
433
|
-
);
|
|
434
|
-
|
|
435
|
-
if (!fsUuid) {
|
|
436
|
-
throw new Error('No se pudo resolver el UUID de FreeSWITCH. ¿La llamada sigue activa?');
|
|
437
|
-
}
|
|
438
|
-
// fsUuid → "53c4b1ef-487c-438a-8254-c99ef17a3a26" (UUID real de FreeSWITCH)
|
|
439
|
-
|
|
440
|
-
// ════════ PASO 3: Agregar Persona B ════════
|
|
441
|
-
const { newCallUuid, originalBridgedLeg } = await voip.addCallParticipant(fsUuid, {
|
|
442
|
-
toNumber: '+17866302522', // Número externo (E.164)
|
|
443
|
-
// toExtension: '1004', // O extensión interna
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
// ⚠️ Persona B está SONANDO (no ha contestado)
|
|
447
|
-
// newCallUuid → UUID del canal de Persona B
|
|
448
|
-
// originalBridgedLeg → UUID de la pata PSTN de Persona A
|
|
449
|
-
|
|
450
|
-
// ════════ PASO 4: Esperar a que Persona B conteste ════════
|
|
451
|
-
// El agente escucha que Persona B contestó, o la UI muestra el botón "Fusionar"
|
|
452
|
-
|
|
453
|
-
// ════════ PASO 5: Fusionar ════════
|
|
454
|
-
const result = await voip.mergeCalls({
|
|
455
|
-
callUuidA: fsUuid, // UUID de FreeSWITCH de la llamada original
|
|
456
|
-
callUuidB: newCallUuid, // UUID de Persona B (de addCallParticipant)
|
|
457
|
-
bridgedLegA: originalBridgedLeg, // UUID de la pata PSTN de Persona A
|
|
458
|
-
conferenceName: undefined, // Auto-generado si no se especifica
|
|
459
|
-
});
|
|
344
|
+
#### MÁQUINA DE ESTADOS de threeWaySession
|
|
460
345
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
346
|
+
```
|
|
347
|
+
null ──addParticipant()──> { state: 'dialing', pendingUuid: '...' }
|
|
348
|
+
│ │
|
|
349
|
+
│ B contesta (polling automático)
|
|
350
|
+
│ │
|
|
351
|
+
│ ▼
|
|
352
|
+
│ { state: 'holding', participants: [...] }
|
|
353
|
+
│ │ │ │
|
|
354
|
+
│ swapParticipant() mergeThreeWay() dropParticipant()
|
|
355
|
+
│ │ │ │
|
|
356
|
+
│ ▼ ▼ │
|
|
357
|
+
│ { state: 'holding' } { state: 'active' }
|
|
358
|
+
│ │ │
|
|
359
|
+
│ swapParticipant() dropParticipant() (≤1 left)
|
|
360
|
+
│ │ │
|
|
361
|
+
│ ▼ ▼
|
|
362
|
+
│ { state: 'holding' } null
|
|
363
|
+
│
|
|
364
|
+
│──cancelAddParticipant()──> null (B killed, A unheld, 2-person conference)
|
|
365
|
+
│
|
|
366
|
+
│──currentCall === null──> null (cleanup automático)
|
|
464
367
|
```
|
|
465
368
|
|
|
466
369
|
---
|
|
467
370
|
|
|
468
|
-
####
|
|
371
|
+
#### Métodos de tres vías — Referencia completa
|
|
469
372
|
|
|
470
|
-
|
|
|
471
|
-
|
|
472
|
-
| `
|
|
473
|
-
| `
|
|
474
|
-
| `
|
|
475
|
-
| `mergeThreeWay
|
|
476
|
-
| `
|
|
477
|
-
| `
|
|
478
|
-
| `swapParticipant` (keepUuid/holdUuid) | `participant.uuid` | De `threeWaySession.participants[i].uuid` |
|
|
479
|
-
| `dropParticipant` (memberId) | `participant.memberId` | De `threeWaySession.participants[i].memberId` |
|
|
373
|
+
| Método | Parámetros | Qué hace |
|
|
374
|
+
|--------|-----------|----------|
|
|
375
|
+
| `addCallParticipant(fsUuid, { toNumber?, toExtension? })` | UUID de FS, destino | Transfiere ambas patas a conferencia, hold A, originate B |
|
|
376
|
+
| `cancelAddParticipant({ conferenceName, newCallUuid, heldMemberUuid })` | Datos de addCallParticipant | Mata B, desmutea A, conferencia 2 personas |
|
|
377
|
+
| `swapCallParticipant({ conferenceName, keepUuid, holdUuid })` | Conference, UUIDs | Mute+deaf+MOH holdUuid, unmute+undeaf keepUuid |
|
|
378
|
+
| `mergeThreeWay({ conferenceName })` | Conference | Desmutea+undeaf TODOS los en hold |
|
|
379
|
+
| `kickParticipant({ conferenceName, memberUuid })` | Conference, UUID | Saca participante de conferencia (cuelga) |
|
|
380
|
+
| `getConferenceMembers(conferenceName)` | Conference | Polling: miembros actuales de la conferencia |
|
|
480
381
|
|
|
481
|
-
|
|
482
|
-
|------------------|---------|
|
|
483
|
-
| `currentCall.id` | Es el ID local de SIP.js, no el UUID de FreeSWITCH |
|
|
484
|
-
| `activeCall.call_uuid` | Es compartido entre A-leg y B-leg, no identifica un canal único |
|
|
382
|
+
#### Deprecated methods
|
|
485
383
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
```
|
|
491
|
-
null ──addParticipant()──> { state: 'adding', pendingUuid: '...' }
|
|
492
|
-
│
|
|
493
|
-
mergeThreeWay()
|
|
494
|
-
│
|
|
495
|
-
▼
|
|
496
|
-
{ state: 'active', participants: [...] }
|
|
497
|
-
│
|
|
498
|
-
swapParticipant()
|
|
499
|
-
│
|
|
500
|
-
▼
|
|
501
|
-
{ state: 'swapping', ... }
|
|
502
|
-
│ (automático al completar)
|
|
503
|
-
▼
|
|
504
|
-
{ state: 'active', participants: [...] }
|
|
505
|
-
│
|
|
506
|
-
dropParticipant() (si quedan ≤1)
|
|
507
|
-
│
|
|
508
|
-
▼
|
|
509
|
-
null
|
|
510
|
-
│
|
|
511
|
-
currentCall === null
|
|
512
|
-
│
|
|
513
|
-
▼
|
|
514
|
-
null (limpieza automática)
|
|
515
|
-
```
|
|
384
|
+
| Método | Reemplazo |
|
|
385
|
+
|--------|-----------|
|
|
386
|
+
| `mergeCalls(params)` | `mergeThreeWay({ conferenceName })` |
|
|
387
|
+
| `mergeDirectCalls(params)` | `addCallParticipant()` + `mergeThreeWay()` |
|
|
516
388
|
|
|
517
389
|
### Extensions
|
|
518
390
|
|