@cemscale-voip/voip-sdk 2.0.10 → 2.0.12
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 +471 -0
- package/dist/client.d.ts +3502 -157
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3492 -121
- package/dist/client.js.map +1 -1
- package/dist/hooks/useVoIP.d.ts.map +1 -1
- package/dist/hooks/useVoIP.js +30 -4
- package/dist/hooks/useVoIP.js.map +1 -1
- package/dist/types.d.ts +1186 -31
- package/dist/types.d.ts.map +1 -1
- package/dist/webrtc.d.ts.map +1 -1
- package/dist/webrtc.js +3 -0
- package/dist/webrtc.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,477 @@ await voip.transfer(callUuid, { targetExtension: '1002', type: 'blind' });
|
|
|
43
43
|
await voip.hangup(callUuid);
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
### Three-Way Calling (Conferencia Tripartita) — GUÍA COMPLETA
|
|
47
|
+
|
|
48
|
+
> **⚠️ LEE ESTO ANTES DE IMPLEMENTAR.** La tripartita tiene un flujo de varios pasos
|
|
49
|
+
> y varios estados de UI. No es una sola llamada API. Si muestras el timer antes
|
|
50
|
+
> de que la persona conteste, o llamas merge antes de que el participante esté listo,
|
|
51
|
+
> TODO el flujo se rompe.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
#### RESUMEN del flujo
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
PASO 0: El agente está en llamada con Persona A. currentCall.state === 'established'.
|
|
59
|
+
PASO 1: El agente presiona "Agregar" → addParticipant('numero de Persona B').
|
|
60
|
+
La API pone a Persona A en hold y origina llamada a Persona B.
|
|
61
|
+
threeWaySession.state → 'adding' ← MOSTRAR "Llamando a Persona B..."
|
|
62
|
+
PASO 2: Persona B contesta su teléfono.
|
|
63
|
+
threeWaySession.state → 'adding' ← se mantiene igual, NO cambia solo
|
|
64
|
+
PASO 3: El agente presiona "Fusionar" → mergeThreeWay().
|
|
65
|
+
Las 3 personas (Agente, A, B) entran a conferencia.
|
|
66
|
+
threeWaySession.state → 'active' ← AHORA sí inicia el timer de conferencia
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
#### FLOW 1 — `addParticipant` + `mergeThreeWay` (WebRTC softphone, recomendado)
|
|
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.
|
|
75
|
+
|
|
76
|
+
##### 1.1 — Configuración inicial
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { useVoIP } from '@cemscale-voip/voip-sdk';
|
|
80
|
+
|
|
81
|
+
// Dentro de tu componente React:
|
|
82
|
+
const {
|
|
83
|
+
// Estado del teléfono
|
|
84
|
+
isRegistered, // boolean — SIP registrado?
|
|
85
|
+
currentCall, // WebRTCCallInfo | null — datos de la llamada activa
|
|
86
|
+
error, // string | null — último error (resetear con clearError)
|
|
87
|
+
|
|
88
|
+
// Acciones de llamada básica
|
|
89
|
+
startPhone, // (ext, password, displayName?) => Promise<void>
|
|
90
|
+
call, // (number) => Promise<WebRTCCallInfo>
|
|
91
|
+
answer, // () => Promise<void>
|
|
92
|
+
hangup, // () => Promise<void>
|
|
93
|
+
toggleHold, // () => Promise<boolean>
|
|
94
|
+
toggleMute, // () => boolean
|
|
95
|
+
|
|
96
|
+
// Acciones de tripartita
|
|
97
|
+
addParticipant, // (target: string) => Promise<void>
|
|
98
|
+
mergeThreeWay, // () => Promise<void>
|
|
99
|
+
swapParticipant, // (keepUuid: string, holdUuid: string) => Promise<void>
|
|
100
|
+
dropParticipant, // (memberId: string) => Promise<void>
|
|
101
|
+
|
|
102
|
+
// Estado de la tripartita
|
|
103
|
+
threeWaySession, // ThreeWaySession | null
|
|
104
|
+
} = useVoIP({
|
|
105
|
+
apiUrl: 'https://voip-api.cemscale.com',
|
|
106
|
+
apiKey: 'csk_live_...', // API key de tu tenant
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
##### 1.2 — `ThreeWaySession` (tipo completo)
|
|
111
|
+
|
|
112
|
+
Este objeto describe TODO el estado de la tripartita. Tu UI debe leerlo para decidir qué mostrar.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
interface ThreeWaySession {
|
|
116
|
+
conferenceName: string; // Nombre de la sala de conferencia (UUID, ej: "page_abc_123")
|
|
117
|
+
state: 'adding' | 'active' | 'swapping';
|
|
118
|
+
// 'adding' = Persona B está siendo llamada. MOSTRAR "Llamando..."
|
|
119
|
+
// 'active' = Conferencia activa. AHORA iniciar timer.
|
|
120
|
+
// 'swapping' = Intercambiando participantes (no mostrar timer propio)
|
|
121
|
+
participants: ThreeWayParticipant[]; // Solo poblado en state='active'
|
|
122
|
+
pendingUuid: string | undefined; // UUID de Persona B. Existe en state='adding'
|
|
123
|
+
originalBridgedLeg: string | undefined; // UUID de la pata PSTN de Persona A
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface ThreeWayParticipant {
|
|
127
|
+
uuid: string; // UUID del canal FreeSWITCH
|
|
128
|
+
callerIdNumber: string; // Número de teléfono
|
|
129
|
+
callerIdName: string; // Nombre (o número si no hay nombre)
|
|
130
|
+
status: 'active' | 'held'; // Estado en la conferencia
|
|
131
|
+
memberId: string; // ID de miembro en la conferencia
|
|
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;
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
##### 1.4 — Flujo COMPLETO paso a paso
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// ═══════════════════════════════════════════════════════════
|
|
156
|
+
// PASO 0: Agente en llamada con Persona A
|
|
157
|
+
// ═══════════════════════════════════════════════════════════
|
|
158
|
+
|
|
159
|
+
// currentCall.state === 'established' ← CONDICIÓN para mostrar botón "Agregar"
|
|
160
|
+
// currentCall.fsChannelUuid !== null ← resuelto automáticamente
|
|
161
|
+
// threeWaySession === null ← no hay tripartita activa
|
|
162
|
+
|
|
163
|
+
// ═══════════════════════════════════════════════════════════
|
|
164
|
+
// PASO 1: Agregar Persona B
|
|
165
|
+
// ═══════════════════════════════════════════════════════════
|
|
166
|
+
|
|
167
|
+
// UI: Botón "Agregar participante" → input para número → llama a addParticipant
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
await addParticipant('+17866302522');
|
|
171
|
+
// └─ Internamente el SDK:
|
|
172
|
+
// 1. Toma currentCall.fsChannelUuid
|
|
173
|
+
// 2. Llama POST /api/calls/{fsChannelUuid}/add-participant
|
|
174
|
+
// 3. API pone Persona A en hold (escucha música)
|
|
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
|
+
}
|
|
190
|
+
|
|
191
|
+
// ═══════════════════════════════════════════════════════════
|
|
192
|
+
// PASO 2: Persona B contesta (el agente lo escucha o ve en UI)
|
|
193
|
+
// ═══════════════════════════════════════════════════════════
|
|
194
|
+
|
|
195
|
+
// El SDK NO notifica automáticamente cuando Persona B contesta.
|
|
196
|
+
// La UI debe mostrar un botón "Fusionar" inmediatamente después de
|
|
197
|
+
// addParticipant. El agente presiona "Fusionar" cuando escucha
|
|
198
|
+
// que Persona B contestó, o cuando ve que pasaron ~10 segundos.
|
|
199
|
+
|
|
200
|
+
// ═══════════════════════════════════════════════════════════
|
|
201
|
+
// PASO 3: Fusionar en conferencia
|
|
202
|
+
// ═══════════════════════════════════════════════════════════
|
|
203
|
+
|
|
204
|
+
// UI: Botón "Fusionar" — visible cuando threeWaySession.state === 'adding'
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
await mergeThreeWay();
|
|
208
|
+
// └─ Internamente el SDK:
|
|
209
|
+
// 1. Toma currentCall.fsChannelUuid (callUuidA)
|
|
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
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
##### 1.5 — Qué mostrar en la UI en CADA estado
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// ── SIN LLAMADA ──
|
|
230
|
+
if (!currentCall) {
|
|
231
|
+
return <Dialpad onCall={call} />;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── LLAMADA ACTIVA, SIN TRIPARTITA ──
|
|
235
|
+
if (currentCall && !threeWaySession) {
|
|
236
|
+
return (
|
|
237
|
+
<InCallScreen>
|
|
238
|
+
<CallTimer startTime={currentCall.answerTime} />
|
|
239
|
+
<RemoteParty name={currentCall.remoteDisplayName} number={currentCall.remoteIdentity} />
|
|
240
|
+
<MuteButton active={currentCall.muted} onPress={toggleMute} />
|
|
241
|
+
<HoldButton active={currentCall.held} onPress={toggleHold} />
|
|
242
|
+
<HangupButton onPress={hangup} />
|
|
243
|
+
<AddParticipantButton onPress={() => showAddParticipantInput()} />
|
|
244
|
+
{/* ↑ Solo mostrar si currentCall.state === 'established' */}
|
|
245
|
+
</InCallScreen>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── AGREGANDO PARTICIPANTE (Persona B está sonando) ──
|
|
250
|
+
if (threeWaySession?.state === 'adding') {
|
|
251
|
+
return (
|
|
252
|
+
<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
|
+
<CallingBanner>
|
|
260
|
+
<Spinner />
|
|
261
|
+
<Text>Llamando a Persona B...</Text>
|
|
262
|
+
{/* ❌ NO mostrar timer para Persona B — no ha contestado */}
|
|
263
|
+
</CallingBanner>
|
|
264
|
+
|
|
265
|
+
<MergeButton onPress={mergeThreeWay} />
|
|
266
|
+
{/* ↑ El agente presiona esto cuando escucha que Persona B contestó */}
|
|
267
|
+
|
|
268
|
+
<CancelButton onPress={hangup} />
|
|
269
|
+
{/* ↑ Cancela todo: cuelga Persona A, Persona B, y al agente */}
|
|
270
|
+
</InCallScreen>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── CONFERENCIA ACTIVA ──
|
|
275
|
+
if (threeWaySession?.state === 'active') {
|
|
276
|
+
return (
|
|
277
|
+
<ConferenceScreen>
|
|
278
|
+
<ConferenceHeader>
|
|
279
|
+
<ConferenceIcon />
|
|
280
|
+
<Text>Conferencia — 3 participantes</Text>
|
|
281
|
+
<ConferenceTimer startTime={conferenceStartTime} />
|
|
282
|
+
{/* ↑ AHORA sí iniciar timer */}
|
|
283
|
+
</ConferenceHeader>
|
|
284
|
+
|
|
285
|
+
{threeWaySession.participants.map(p => (
|
|
286
|
+
<ParticipantRow key={p.uuid}>
|
|
287
|
+
<ParticipantName name={p.callerIdName} number={p.callerIdNumber} />
|
|
288
|
+
<ParticipantStatus status={p.status} />
|
|
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)} />
|
|
293
|
+
</ParticipantRow>
|
|
294
|
+
))}
|
|
295
|
+
|
|
296
|
+
<MuteButton active={currentCall.muted} onPress={toggleMute} />
|
|
297
|
+
<EndConferenceButton onPress={hangup} />
|
|
298
|
+
{/* ↑ Cuelga a TODOS y destruye la conferencia */}
|
|
299
|
+
</ConferenceScreen>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
##### 1.6 — Errores comunes y cómo manejarlos
|
|
305
|
+
|
|
306
|
+
| Error | Causa | Solución |
|
|
307
|
+
|-------|-------|----------|
|
|
308
|
+
| `"FreeSWITCH UUID not resolved yet"` | Llamaste `addParticipant` antes de que la llamada conecte | Espera a `currentCall.state === 'established'` y `currentCall.fsChannelUuid !== null` |
|
|
309
|
+
| `"No active call"` | `currentCall` es null | Verifica que haya una llamada activa antes de mostrar el botón |
|
|
310
|
+
| `"No pending participant to merge"` | `threeWaySession.pendingUuid` es undefined | Solo mostrar botón "Fusionar" cuando `threeWaySession?.state === 'adding'` y `threeWaySession?.pendingUuid` existe |
|
|
311
|
+
| Timer se inicia antes de contestar | Tu UI inicia el timer en state='adding' | Solo iniciar timer cuando `threeWaySession.state === 'active'` |
|
|
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. |
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
#### FLOW 2 — `mergeDirectCalls` (teléfono físico / desk phone)
|
|
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.
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
import { VoIPClient } from '@cemscale-voip/voip-sdk';
|
|
329
|
+
|
|
330
|
+
const voip = new VoIPClient({
|
|
331
|
+
apiUrl: 'https://voip-api.cemscale.com',
|
|
332
|
+
apiKey: 'csk_live_...',
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// ═══════════════════════════════════════════════════════
|
|
336
|
+
// PASO 1: Detectar 2+ llamadas activas de la misma extensión
|
|
337
|
+
// ═══════════════════════════════════════════════════════
|
|
338
|
+
|
|
339
|
+
// Hacer polling cada 5 segundos
|
|
340
|
+
const pollInterval = setInterval(async () => {
|
|
341
|
+
const { activeCalls } = await voip.getActiveCalls();
|
|
342
|
+
|
|
343
|
+
// Filtrar llamadas de la extensión del agente
|
|
344
|
+
const agentExt = '1001';
|
|
345
|
+
const myCalls = activeCalls.filter(c =>
|
|
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);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ═══════════════════════════════════════════════════════
|
|
379
|
+
// LIMPIEZA
|
|
380
|
+
// ═══════════════════════════════════════════════════════
|
|
381
|
+
|
|
382
|
+
// Cuando el componente se desmonta:
|
|
383
|
+
clearInterval(pollInterval);
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
##### 2.1 — Formato de `ActiveCall` (de `getActiveCalls()`)
|
|
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
|
+
```
|
|
401
|
+
|
|
402
|
+
##### 2.2 — IMPORTANTE: No confundir los UUIDs
|
|
403
|
+
|
|
404
|
+
```
|
|
405
|
+
activeCall.uuid → "a1b2c3d4-..." ← USAR ESTE para mergeDirectCalls
|
|
406
|
+
activeCall.call_uuid → "a1b2c3d4-..." ← NO USAR (es el mismo que el de la otra pata)
|
|
407
|
+
|
|
408
|
+
Una llamada tiene 2 canales (A-leg y B-leg) que comparten el MISMO call_uuid
|
|
409
|
+
pero tienen DIFERENTE uuid. mergeDirectCalls necesita los uuid de canal.
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
#### FLOW 3 — `addCallParticipant` + `mergeCalls` sin hook (VoIPClient directo)
|
|
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
|
+
});
|
|
460
|
+
|
|
461
|
+
// result.conferenceName → "page_15d0851a_..."
|
|
462
|
+
// result.participants → [{ callerIdNumber, callerIdName, uuid, status, memberId }, ...]
|
|
463
|
+
// result.state → 'active'
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
#### RESUMEN de estados y UUIDs
|
|
469
|
+
|
|
470
|
+
| Contexto | Qué UUID usar | De dónde viene |
|
|
471
|
+
|----------|--------------|----------------|
|
|
472
|
+
| `addParticipant` | `currentCall.fsChannelUuid` | Resuelto automático por useVoIP al conectar |
|
|
473
|
+
| `mergeThreeWay` (callUuidA) | `currentCall.fsChannelUuid` | Mismo que arriba |
|
|
474
|
+
| `mergeThreeWay` (callUuidB) | `threeWaySession.pendingUuid` | Retornado por `addParticipant` |
|
|
475
|
+
| `mergeThreeWay` (bridgedLegA) | `threeWaySession.originalBridgedLeg` | Retornado por `addParticipant` |
|
|
476
|
+
| `mergeDirectCalls` (callUuidA/B) | `activeCall.uuid` | De `getActiveCalls()` |
|
|
477
|
+
| `resolveCallFsUuid` (manual) | Retorna `string \| null` | Lo usas para `addCallParticipant` |
|
|
478
|
+
| `swapParticipant` (keepUuid/holdUuid) | `participant.uuid` | De `threeWaySession.participants[i].uuid` |
|
|
479
|
+
| `dropParticipant` (memberId) | `participant.memberId` | De `threeWaySession.participants[i].memberId` |
|
|
480
|
+
|
|
481
|
+
| ❌ NO USAR NUNCA | Por qué |
|
|
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 |
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
#### MÁQUINA DE ESTADOS de threeWaySession
|
|
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
|
+
```
|
|
516
|
+
|
|
46
517
|
### Extensions
|
|
47
518
|
|
|
48
519
|
```typescript
|