@z-api/call 1.0.0-staging.0
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 +590 -0
- package/dist/core/AudioEngine.d.ts +25 -0
- package/dist/core/AudioEngine.d.ts.map +1 -0
- package/dist/core/CallState.d.ts +15 -0
- package/dist/core/CallState.d.ts.map +1 -0
- package/dist/core/CallWebSocket.d.ts +29 -0
- package/dist/core/CallWebSocket.d.ts.map +1 -0
- package/dist/core/EventEmitter.d.ts +12 -0
- package/dist/core/EventEmitter.d.ts.map +1 -0
- package/dist/core/VideoEngine.d.ts +37 -0
- package/dist/core/VideoEngine.d.ts.map +1 -0
- package/dist/core/ZAPICallClient.d.ts +39 -0
- package/dist/core/ZAPICallClient.d.ts.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/format.d.ts +5 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/pcm.d.ts +7 -0
- package/dist/utils/pcm.d.ts.map +1 -0
- package/dist/widget/CallWidget.css.d.ts +3 -0
- package/dist/widget/CallWidget.css.d.ts.map +1 -0
- package/dist/widget/CallWidget.d.ts +86 -0
- package/dist/widget/CallWidget.d.ts.map +1 -0
- package/dist/widget/icons.d.ts +16 -0
- package/dist/widget/icons.d.ts.map +1 -0
- package/dist/z-api-call.global.js +915 -0
- package/dist/z-api-call.global.js.map +1 -0
- package/dist/z-api-call.mjs +1830 -0
- package/dist/z-api-call.mjs.map +1 -0
- package/dist/z-api-call.umd.js +915 -0
- package/dist/z-api-call.umd.js.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
# @z-api/call
|
|
2
|
+
|
|
3
|
+
SDK para integrar chamadas VoIP do WhatsApp em aplicações web. Fornece um widget pronto (webphone) com discador, lista de chamadas e tela de ligação ativa, além de uma API programática completa.
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @z-api/call
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Via CDN (script tag)
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<script src="https://unpkg.com/@z-api/call/dist/z-api-call.global.js"></script>
|
|
15
|
+
<script>
|
|
16
|
+
const client = ZAPICall.init({
|
|
17
|
+
instanceId: 'SUA_INSTANCE_ID',
|
|
18
|
+
getToken: async () => {
|
|
19
|
+
const res = await fetch('https://seu-backend.com/call-token');
|
|
20
|
+
const data = await res.json();
|
|
21
|
+
return data.token;
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
</script>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { init } from '@z-api/call';
|
|
31
|
+
|
|
32
|
+
const client = init({
|
|
33
|
+
instanceId: 'SUA_INSTANCE_ID',
|
|
34
|
+
getToken: async () => {
|
|
35
|
+
// Chama SEU backend, que por sua vez chama a Z-API para obter o token efêmero
|
|
36
|
+
const res = await fetch('https://seu-backend.com/call-token');
|
|
37
|
+
const data = await res.json();
|
|
38
|
+
return data.token;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Ouvir eventos
|
|
43
|
+
client.on('call:incoming', (call) => {
|
|
44
|
+
console.log('Chamada recebida de', call.from);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
client.on('connected', () => {
|
|
48
|
+
console.log('Conectado ao servidor');
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Autenticação (getToken)
|
|
53
|
+
|
|
54
|
+
A SDK utiliza tokens efêmeros para autenticação. Em vez de passar um token fixo, você fornece uma função `getToken` que é chamada sempre que a SDK precisa se conectar (inclusive em reconexões).
|
|
55
|
+
|
|
56
|
+
### Fluxo de autenticação
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
┌──────────┐ getToken() ┌───────────────┐ POST /call-token ┌─────────────┐
|
|
60
|
+
│ SDK │ ──────────────────► │ Seu Backend │ ──────────────────────► │ Z-API │
|
|
61
|
+
│ (browser)│ │ │ │ │
|
|
62
|
+
│ │ ◄────────────────── │ │ ◄────────────────────── │ │
|
|
63
|
+
│ │ token efêmero │ │ token efêmero │ │
|
|
64
|
+
└──────────┘ └───────────────┘ └─────────────┘
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
1. A SDK chama a função `getToken` fornecida por você
|
|
68
|
+
2. Sua função faz uma requisição ao **seu backend**
|
|
69
|
+
3. Seu backend chama a API da Z-API para gerar o token efêmero:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
POST https://api.z-api.io/instances/{ID_INSTANCE}/token/{TOKEN_INSTANCE}/call-token
|
|
73
|
+
Header: client-token: SEU_CLIENT_TOKEN
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
4. A Z-API retorna o token efêmero
|
|
77
|
+
5. Seu backend repassa o token para o frontend
|
|
78
|
+
6. A SDK usa o token para conectar via WebSocket
|
|
79
|
+
|
|
80
|
+
### Exemplo de backend (Node.js)
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
app.get('/call-token', async (req, res) => {
|
|
84
|
+
const response = await fetch(
|
|
85
|
+
`https://api.z-api.io/instances/${ID_INSTANCE}/token/${TOKEN_INSTANCE}/call-token`,
|
|
86
|
+
{
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'client-token': SEU_CLIENT_TOKEN },
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
const data = await response.json();
|
|
92
|
+
res.json({ token: data.token });
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Exemplo de getToken no frontend
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const client = init({
|
|
100
|
+
instanceId: 'SUA_INSTANCE_ID',
|
|
101
|
+
getToken: async () => {
|
|
102
|
+
const res = await fetch('https://seu-backend.com/call-token');
|
|
103
|
+
const { token } = await res.json();
|
|
104
|
+
return token;
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
> **Importante:** Nunca exponha o `TOKEN_INSTANCE` ou `client-token` no frontend. A chamada à Z-API deve ser feita pelo seu backend.
|
|
110
|
+
|
|
111
|
+
## Opções de Inicialização
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const client = init({
|
|
115
|
+
// --- Obrigatórios ---
|
|
116
|
+
instanceId: string, // ID da instância Z-API
|
|
117
|
+
getToken: () => Promise<string>, // Função que retorna o token efêmero
|
|
118
|
+
|
|
119
|
+
// --- Opcionais ---
|
|
120
|
+
autoWidget: true, // Criar widget automaticamente (default: true)
|
|
121
|
+
showVideoDialButton: false, // Exibir botão de vídeo chamada no discador (default: false)
|
|
122
|
+
position: 'bottom-right', // Posição do widget (ver abaixo)
|
|
123
|
+
theme: { ... }, // Personalização visual (ver Temas)
|
|
124
|
+
|
|
125
|
+
// --- Callbacks ---
|
|
126
|
+
onIncomingCall: (call) => {}, // Chamada recebida
|
|
127
|
+
onCallTerminated: (callId) => {}, // Chamada encerrada
|
|
128
|
+
onConnectionChange: (connected) => {}, // Conexão mudou
|
|
129
|
+
onMakeCall: (number) => {}, // Chamada de saída iniciada
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Posições do Widget
|
|
134
|
+
|
|
135
|
+
| Valor | Posição |
|
|
136
|
+
|-------|---------|
|
|
137
|
+
| `'bottom-right'` | Canto inferior direito (default) |
|
|
138
|
+
| `'bottom-left'` | Canto inferior esquerdo |
|
|
139
|
+
| `'top-right'` | Canto superior direito |
|
|
140
|
+
| `'top-left'` | Canto superior esquerdo |
|
|
141
|
+
|
|
142
|
+
## Temas
|
|
143
|
+
|
|
144
|
+
### Dark / Light Mode
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// Tema escuro (default)
|
|
148
|
+
const client = init({
|
|
149
|
+
instanceId: '...',
|
|
150
|
+
getToken: async () => { /* ... */ },
|
|
151
|
+
theme: { mode: 'dark' },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Tema claro
|
|
155
|
+
const client = init({
|
|
156
|
+
instanceId: '...',
|
|
157
|
+
getToken: async () => { /* ... */ },
|
|
158
|
+
theme: { mode: 'light' },
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Personalização Completa
|
|
163
|
+
|
|
164
|
+
Todas as propriedades são opcionais. Os valores default dependem do `mode`.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
theme: {
|
|
168
|
+
mode: 'dark', // 'dark' | 'light'
|
|
169
|
+
primaryColor: '#00a884', // Cor principal (botões, destaques)
|
|
170
|
+
dangerColor: '#f44336', // Cor de perigo (rejeitar, encerrar)
|
|
171
|
+
backgroundColor: '#202c33',// Fundo dos cards e numpad
|
|
172
|
+
surfaceColor: '#111b21', // Fundo do painel
|
|
173
|
+
textColor: '#e9edef', // Cor do texto principal
|
|
174
|
+
textSecondaryColor: '#8696a0', // Cor do texto secundário
|
|
175
|
+
borderRadius: '16px', // Raio de borda do painel
|
|
176
|
+
fontFamily: 'Inter, sans-serif', // Família de fontes
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Defaults por modo:**
|
|
181
|
+
|
|
182
|
+
| Propriedade | Dark | Light |
|
|
183
|
+
|-------------|------|-------|
|
|
184
|
+
| `backgroundColor` | `#202c33` | `#f0f2f5` |
|
|
185
|
+
| `surfaceColor` | `#111b21` | `#ffffff` |
|
|
186
|
+
| `textColor` | `#e9edef` | `#111b21` |
|
|
187
|
+
| `textSecondaryColor` | `#8696a0` | `#667781` |
|
|
188
|
+
|
|
189
|
+
## API do Cliente
|
|
190
|
+
|
|
191
|
+
### Métodos
|
|
192
|
+
|
|
193
|
+
#### `accept(callId: string): void`
|
|
194
|
+
|
|
195
|
+
Aceita uma chamada recebida.
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
client.on('call:incoming', (call) => {
|
|
199
|
+
client.accept(call.callId);
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### `reject(callId: string): void`
|
|
204
|
+
|
|
205
|
+
Rejeita ou encerra uma chamada.
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
client.reject(callId);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### `makeCall(number: string, isVideo?: boolean): void`
|
|
212
|
+
|
|
213
|
+
Inicia uma chamada de saída (áudio ou vídeo).
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
client.makeCall('5511999999999'); // chamada de áudio
|
|
217
|
+
client.makeCall('5511999999999', true); // chamada de vídeo
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
> **Nota:** Requer suporte no backend. A SDK envia o evento `call:make` via WebSocket.
|
|
221
|
+
|
|
222
|
+
#### `setCamera(enabled: boolean): Promise<void>`
|
|
223
|
+
|
|
224
|
+
Liga ou desliga a câmera durante uma chamada. Se a chamada for de áudio, automaticamente solicita upgrade para vídeo.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
await client.setCamera(true); // ligar câmera
|
|
228
|
+
await client.setCamera(false); // desligar câmera
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
#### `isCameraActive(): boolean`
|
|
232
|
+
|
|
233
|
+
Retorna se a câmera está ativa.
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
if (client.isCameraActive()) {
|
|
237
|
+
console.log('Câmera ligada');
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### `mute(muted: boolean): void`
|
|
242
|
+
|
|
243
|
+
Ativa/desativa o microfone.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
client.mute(true); // mutar
|
|
247
|
+
client.mute(false); // desmutar
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### `getCalls(): CallInfo[]`
|
|
251
|
+
|
|
252
|
+
Retorna todas as chamadas ativas.
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
const calls = client.getCalls();
|
|
256
|
+
calls.forEach(c => console.log(c.callId, c.state));
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### `getActiveCall(): CallInfo | undefined`
|
|
260
|
+
|
|
261
|
+
Retorna a chamada ativa (accepted/active) se houver.
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
const call = client.getActiveCall();
|
|
265
|
+
if (call) {
|
|
266
|
+
console.log('Em ligação com', call.from);
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### `isConnected(): boolean`
|
|
271
|
+
|
|
272
|
+
Verifica se está conectado ao servidor.
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
if (client.isConnected()) {
|
|
276
|
+
console.log('Online');
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### `destroy(): void`
|
|
281
|
+
|
|
282
|
+
Destrói o cliente, fecha conexões e remove o widget do DOM.
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
client.destroy();
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Eventos
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
// Conexão estabelecida
|
|
292
|
+
client.on('connected', () => { });
|
|
293
|
+
|
|
294
|
+
// Conexão perdida
|
|
295
|
+
client.on('disconnected', () => { });
|
|
296
|
+
|
|
297
|
+
// Chamada recebida
|
|
298
|
+
client.on('call:incoming', (call: CallInfo) => {
|
|
299
|
+
console.log(call.callId, call.from, call.isVideo);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Estado da chamada mudou
|
|
303
|
+
client.on('call:state', (callId: string, state: CallState) => {
|
|
304
|
+
// state: 'ringing' | 'preaccepted' | 'accepted' | 'active' | 'ended'
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Chamada encerrada
|
|
308
|
+
client.on('call:terminated', (callId: string, reason?: string) => { });
|
|
309
|
+
|
|
310
|
+
// Vídeo ativado/desativado durante a chamada (upgrade/downgrade)
|
|
311
|
+
client.on('call:video-state', (callId: string, isVideo: boolean) => {
|
|
312
|
+
console.log(isVideo ? 'Chamada mudou para vídeo' : 'Chamada voltou para áudio');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Chamada de saída iniciada
|
|
316
|
+
client.on('call:outgoing', (call: CallInfo) => { });
|
|
317
|
+
|
|
318
|
+
// Chamada de saída iniciada (número)
|
|
319
|
+
client.on('call:make', (number: string) => { });
|
|
320
|
+
|
|
321
|
+
// Nível de áudio (0 a 1)
|
|
322
|
+
client.on('audio:level', (level: number) => {
|
|
323
|
+
console.log('Volume:', Math.round(level * 100) + '%');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Erro
|
|
327
|
+
client.on('error', (error: Error) => {
|
|
328
|
+
console.error(error.message);
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Para remover um listener:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
const handler = (call) => console.log(call);
|
|
336
|
+
client.on('call:incoming', handler);
|
|
337
|
+
client.off('call:incoming', handler);
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Widget
|
|
341
|
+
|
|
342
|
+
O widget é criado automaticamente quando `autoWidget` está ativo (default). Ele renderiza dentro de um Shadow DOM isolado, sem conflitos de CSS com a aplicação host.
|
|
343
|
+
|
|
344
|
+
### Estrutura
|
|
345
|
+
|
|
346
|
+
O widget possui 3 views:
|
|
347
|
+
|
|
348
|
+
**Discador** — Teclado numérico para digitar e iniciar chamadas.
|
|
349
|
+
|
|
350
|
+
**Chamadas** — Lista de chamadas ativas com botões de aceitar/rejeitar/encerrar.
|
|
351
|
+
|
|
352
|
+
**Ligação Ativa** — Tela fullscreen (no painel) com avatar, número, timer, barra de áudio e controles de mute/encerrar.
|
|
353
|
+
|
|
354
|
+
### Comportamento Automático
|
|
355
|
+
|
|
356
|
+
- Abre o painel automaticamente ao receber uma chamada
|
|
357
|
+
- Muda para a view "Chamadas" quando tem chamada entrando
|
|
358
|
+
- Muda para a view "Ligação Ativa" quando a chamada é aceita
|
|
359
|
+
- Volta para "Discador" quando todas as chamadas encerram
|
|
360
|
+
- Badge no bubble mostra número de chamadas ativas
|
|
361
|
+
- Dot verde/vermelho indica status da conexão
|
|
362
|
+
- Pulse animation no bubble quando tem chamada tocando
|
|
363
|
+
|
|
364
|
+
### Modo Headless
|
|
365
|
+
|
|
366
|
+
Para controlar tudo manualmente sem widget:
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
const client = init({
|
|
370
|
+
instanceId: '...',
|
|
371
|
+
getToken: async () => { /* ... */ },
|
|
372
|
+
autoWidget: false,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Gerencie chamadas via API
|
|
376
|
+
client.on('call:incoming', (call) => {
|
|
377
|
+
// Sua UI customizada
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Tipos
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
interface CallInfo {
|
|
385
|
+
callId: string; // ID único da chamada
|
|
386
|
+
from: string; // Número de origem
|
|
387
|
+
isVideo: boolean; // Se é vídeo chamada
|
|
388
|
+
isGroup: boolean; // Se é chamada de grupo
|
|
389
|
+
state: CallState; // Estado atual
|
|
390
|
+
timestamp: number; // Timestamp de início
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
type CallState = 'ringing' | 'preaccepted' | 'accepted' | 'active' | 'ended';
|
|
394
|
+
|
|
395
|
+
type WidgetPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
396
|
+
|
|
397
|
+
type ThemeMode = 'dark' | 'light';
|
|
398
|
+
|
|
399
|
+
interface AudioConfig {
|
|
400
|
+
sampleRate: number; // Taxa de amostragem (ex: 16000)
|
|
401
|
+
channels: number; // Canais de áudio (1 = mono)
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Protocolo WebSocket
|
|
406
|
+
|
|
407
|
+
A SDK se conecta automaticamente ao servidor via WebSocket e gerencia reconexão com backoff exponencial. O token efêmero obtido via `getToken` é utilizado em cada conexão/reconexão.
|
|
408
|
+
|
|
409
|
+
### URL de Conexão
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
wss://call.z-api.io/ws/call?instance=SUA_INSTANCE_ID&token=TOKEN_EFEMERO
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Mensagens Client -> Server
|
|
416
|
+
|
|
417
|
+
| Tipo | Payload | Descrição |
|
|
418
|
+
|------|---------|-----------|
|
|
419
|
+
| `call:accept` | `{ callId }` | Aceitar chamada |
|
|
420
|
+
| `call:reject` | `{ callId }` | Rejeitar/encerrar chamada |
|
|
421
|
+
| `call:mute` | `{ muted }` | Mutar/desmutar mic |
|
|
422
|
+
| `call:camera` | `{ enabled }` | Ligar/desligar câmera (upgrade para vídeo se necessário) |
|
|
423
|
+
| `call:make` | `{ number, isVideo? }` | Iniciar chamada de saída (áudio ou vídeo) |
|
|
424
|
+
| `ping` | `{}` | Keep-alive (automático, 25s) |
|
|
425
|
+
| *binary 0x01* | `ArrayBuffer` | Áudio PCM Int16LE do microfone |
|
|
426
|
+
| *binary 0x02* | `ArrayBuffer` | Frame de vídeo NV12 (width u16 + height u16 + data) |
|
|
427
|
+
|
|
428
|
+
### Mensagens Server -> Client
|
|
429
|
+
|
|
430
|
+
| Tipo | Payload | Descrição |
|
|
431
|
+
|------|---------|-----------|
|
|
432
|
+
| `connected` | `{ instanceId, sdkId, audioConfig }` | Conexão estabelecida |
|
|
433
|
+
| `call:incoming` | `{ callId, from, isVideo, isGroup, state?, timestamp }` | Chamada recebida |
|
|
434
|
+
| `call:outgoing` | `{ callId, to, isVideo }` | Chamada de saída iniciada |
|
|
435
|
+
| `call:state` | `{ callId, state }` | Mudança de estado |
|
|
436
|
+
| `call:video-state` | `{ callId, isVideo }` | Vídeo ativado/desativado durante chamada |
|
|
437
|
+
| `call:terminated` | `{ callId, reason? }` | Chamada encerrada |
|
|
438
|
+
| `call:claimed` | `{ callId }` | Chamada aceita por outro cliente |
|
|
439
|
+
| `call:dismissed` | `{ callId }` | Chamada rejeitada por outro cliente |
|
|
440
|
+
| `pong` | `{ ts }` | Resposta ao ping |
|
|
441
|
+
| `error` | `{ command, message }` | Erro de processamento |
|
|
442
|
+
| *binary 0x00* | `ArrayBuffer` | Áudio PCM Int16LE para playback |
|
|
443
|
+
| *binary 0x01* | `ArrayBuffer` | Frame de vídeo (header 16 bytes + pixel data) |
|
|
444
|
+
|
|
445
|
+
## Áudio
|
|
446
|
+
|
|
447
|
+
A SDK gerencia captura e reprodução de áudio automaticamente:
|
|
448
|
+
|
|
449
|
+
- **Formato:** PCM Int16LE, mono
|
|
450
|
+
- **Sample rate:** Configurável pelo servidor (default: 16000 Hz)
|
|
451
|
+
- **Captura:** Microfone com echo cancellation, noise suppression e auto gain
|
|
452
|
+
- **Playback:** Web Audio API com scheduling otimizado
|
|
453
|
+
- **Permissão:** O microfone é solicitado automaticamente ao aceitar uma chamada
|
|
454
|
+
|
|
455
|
+
> **Importante:** Navegadores exigem interação do usuário antes de reproduzir áudio. A SDK chama `AudioContext.resume()` automaticamente quando o usuário clica no widget.
|
|
456
|
+
|
|
457
|
+
## Vídeo
|
|
458
|
+
|
|
459
|
+
A SDK suporta chamadas de vídeo e upgrade de áudio para vídeo durante a chamada:
|
|
460
|
+
|
|
461
|
+
- **Formato de recepção:** NV12 com header (width, height, format, orientation, keyframe, timestamp)
|
|
462
|
+
- **Formato de envio:** NV12 (RGBA capturado da câmera, convertido para NV12 no browser)
|
|
463
|
+
- **Renderização:** WebCodecs API (hardware-accelerated) com fallback para software
|
|
464
|
+
- **Câmera:** Captura a 320x240 @ 30fps (default)
|
|
465
|
+
- **Orientação:** Suporte a rotação 0/90/180/270 graus
|
|
466
|
+
|
|
467
|
+
### Fluxos suportados
|
|
468
|
+
|
|
469
|
+
| Cenário | Comportamento |
|
|
470
|
+
|---------|--------------|
|
|
471
|
+
| Chamada inicia como vídeo | Câmera liga automaticamente ao aceitar |
|
|
472
|
+
| Chamada inicia como áudio, peer muda para vídeo | Vídeo remoto é exibido, câmera liga pelo botão |
|
|
473
|
+
| Chamada inicia como áudio, usuário muda para vídeo | Clica no botão de câmera, envia upgrade request ao peer |
|
|
474
|
+
| Discador com botão de vídeo | Habilitado via `showVideoDialButton: true` |
|
|
475
|
+
|
|
476
|
+
## Exemplos
|
|
477
|
+
|
|
478
|
+
### Integração Básica
|
|
479
|
+
|
|
480
|
+
```html
|
|
481
|
+
<!DOCTYPE html>
|
|
482
|
+
<html>
|
|
483
|
+
<head>
|
|
484
|
+
<title>Meu Webphone</title>
|
|
485
|
+
</head>
|
|
486
|
+
<body>
|
|
487
|
+
<h1>Minha Aplicação</h1>
|
|
488
|
+
<script src="https://unpkg.com/@z-api/call/dist/z-api-call.global.js"></script>
|
|
489
|
+
<script>
|
|
490
|
+
const client = ZAPICall.init({
|
|
491
|
+
instanceId: 'minha-instancia',
|
|
492
|
+
getToken: async () => {
|
|
493
|
+
const res = await fetch('https://seu-backend.com/call-token');
|
|
494
|
+
const data = await res.json();
|
|
495
|
+
return data.token;
|
|
496
|
+
},
|
|
497
|
+
theme: { mode: 'light' },
|
|
498
|
+
});
|
|
499
|
+
</script>
|
|
500
|
+
</body>
|
|
501
|
+
</html>
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### React
|
|
505
|
+
|
|
506
|
+
```tsx
|
|
507
|
+
import { useEffect, useRef } from 'react';
|
|
508
|
+
import { init, ZAPICallClient } from '@z-api/call';
|
|
509
|
+
|
|
510
|
+
function App() {
|
|
511
|
+
const clientRef = useRef<ZAPICallClient | null>(null);
|
|
512
|
+
|
|
513
|
+
useEffect(() => {
|
|
514
|
+
const client = init({
|
|
515
|
+
instanceId: 'minha-instancia',
|
|
516
|
+
getToken: async () => {
|
|
517
|
+
const res = await fetch('/api/call-token');
|
|
518
|
+
const { token } = await res.json();
|
|
519
|
+
return token;
|
|
520
|
+
},
|
|
521
|
+
position: 'bottom-left',
|
|
522
|
+
theme: { mode: 'dark', primaryColor: '#7c3aed' },
|
|
523
|
+
onIncomingCall: (call) => {
|
|
524
|
+
console.log('Chamada de', call.from);
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
clientRef.current = client;
|
|
529
|
+
return () => client.destroy();
|
|
530
|
+
}, []);
|
|
531
|
+
|
|
532
|
+
return <div>Minha App</div>;
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Headless com UI Customizada
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
import { init } from '@z-api/call';
|
|
540
|
+
|
|
541
|
+
const client = init({
|
|
542
|
+
instanceId: '...',
|
|
543
|
+
getToken: async () => {
|
|
544
|
+
const res = await fetch('/api/call-token');
|
|
545
|
+
const { token } = await res.json();
|
|
546
|
+
return token;
|
|
547
|
+
},
|
|
548
|
+
autoWidget: false,
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
client.on('connected', () => {
|
|
552
|
+
document.getElementById('status')!.textContent = 'Online';
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
client.on('call:incoming', (call) => {
|
|
556
|
+
const accept = confirm(`Chamada de ${call.from}. Aceitar?`);
|
|
557
|
+
if (accept) {
|
|
558
|
+
client.accept(call.callId);
|
|
559
|
+
} else {
|
|
560
|
+
client.reject(call.callId);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
client.on('call:state', (callId, state) => {
|
|
565
|
+
document.getElementById('call-state')!.textContent = state;
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
client.on('audio:level', (level) => {
|
|
569
|
+
document.getElementById('meter')!.style.width = `${level * 100}%`;
|
|
570
|
+
});
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
## Compatibilidade
|
|
574
|
+
|
|
575
|
+
- Chrome 80+
|
|
576
|
+
- Firefox 78+
|
|
577
|
+
- Safari 14+
|
|
578
|
+
- Edge 80+
|
|
579
|
+
|
|
580
|
+
Requer suporte a:
|
|
581
|
+
- WebSocket
|
|
582
|
+
- Web Audio API
|
|
583
|
+
- getUserMedia (para microfone e câmera)
|
|
584
|
+
- Shadow DOM
|
|
585
|
+
- Canvas 2D (para renderização de vídeo)
|
|
586
|
+
- WebCodecs API (opcional, para renderização acelerada por hardware)
|
|
587
|
+
|
|
588
|
+
## Licença
|
|
589
|
+
|
|
590
|
+
Proprietary - Four Pixel IT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type OnMicData = (pcmInt16: ArrayBuffer) => void;
|
|
2
|
+
export type OnAudioLevel = (level: number) => void;
|
|
3
|
+
export declare class AudioEngine {
|
|
4
|
+
private playbackCtx;
|
|
5
|
+
private micCtx;
|
|
6
|
+
private micStream;
|
|
7
|
+
private micProcessor;
|
|
8
|
+
private micSource;
|
|
9
|
+
private nextPlayTime;
|
|
10
|
+
private sampleRate;
|
|
11
|
+
private destroyed;
|
|
12
|
+
onMicData: OnMicData;
|
|
13
|
+
onAudioLevel: OnAudioLevel;
|
|
14
|
+
constructor(sampleRate?: number);
|
|
15
|
+
/** Play received PCM Int16LE audio */
|
|
16
|
+
private playLogCount;
|
|
17
|
+
playPcm(data: ArrayBuffer): void;
|
|
18
|
+
/** Start capturing microphone audio and sending as PCM Int16LE */
|
|
19
|
+
startMic(): Promise<void>;
|
|
20
|
+
stopMic(): void;
|
|
21
|
+
/** Resume AudioContext (must be called from user gesture for autoplay policy) */
|
|
22
|
+
resume(): void;
|
|
23
|
+
destroy(): void;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=AudioEngine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AudioEngine.d.ts","sourceRoot":"","sources":["../../src/core/AudioEngine.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,SAAS,GAAG,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;AACxD,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;AAEnD,qBAAa,WAAW;IACtB,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,YAAY,CAAoC;IACxD,OAAO,CAAC,SAAS,CAA2C;IAC5D,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAS;IAE1B,SAAS,EAAE,SAAS,CAAY;IAChC,YAAY,EAAE,YAAY,CAAY;gBAE1B,UAAU,SAAQ;IAI9B,sCAAsC;IACtC,OAAO,CAAC,YAAY,CAAK;IAEzB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAgDhC,kEAAkE;IAC5D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IA6B/B,OAAO,IAAI,IAAI;IAqBf,iFAAiF;IACjF,MAAM,IAAI,IAAI;IAUd,OAAO,IAAI,IAAI;CAQhB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CallInfo, CallState } from '../types';
|
|
2
|
+
export declare class CallStateManager {
|
|
3
|
+
private calls;
|
|
4
|
+
addCall(call: CallInfo): void;
|
|
5
|
+
updateState(callId: string, state: CallState): void;
|
|
6
|
+
updateIsVideo(callId: string, isVideo: boolean): void;
|
|
7
|
+
removeCall(callId: string): void;
|
|
8
|
+
getCall(callId: string): CallInfo | undefined;
|
|
9
|
+
getActiveCall(): CallInfo | undefined;
|
|
10
|
+
getRingingCalls(): CallInfo[];
|
|
11
|
+
getAllCalls(): CallInfo[];
|
|
12
|
+
hasActiveCalls(): boolean;
|
|
13
|
+
clear(): void;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=CallState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CallState.d.ts","sourceRoot":"","sources":["../../src/core/CallState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAEpD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,KAAK,CAA+B;IAE5C,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAI7B,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI;IASnD,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAOrD,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIhC,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAI7C,aAAa,IAAI,QAAQ,GAAG,SAAS;IASrC,eAAe,IAAI,QAAQ,EAAE;IAU7B,WAAW,IAAI,QAAQ,EAAE;IAIzB,cAAc,IAAI,OAAO;IAOzB,KAAK,IAAI,IAAI;CAGd"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { WsServerMessage } from '../types';
|
|
2
|
+
export type OnJsonMessage = (msg: WsServerMessage) => void;
|
|
3
|
+
export type OnBinaryMessage = (data: ArrayBuffer) => void;
|
|
4
|
+
export type OnConnectionChange = (connected: boolean) => void;
|
|
5
|
+
export declare class CallWebSocket {
|
|
6
|
+
private ws;
|
|
7
|
+
private baseUrl;
|
|
8
|
+
private instanceId;
|
|
9
|
+
private getToken;
|
|
10
|
+
private reconnectMs;
|
|
11
|
+
private reconnectTimer;
|
|
12
|
+
private pingTimer;
|
|
13
|
+
private destroyed;
|
|
14
|
+
onJson: OnJsonMessage;
|
|
15
|
+
onBinary: OnBinaryMessage;
|
|
16
|
+
onConnectionChange: OnConnectionChange;
|
|
17
|
+
constructor(baseUrl: string, instanceId: string, getToken: () => Promise<string>);
|
|
18
|
+
private buildUrl;
|
|
19
|
+
connect(): Promise<void>;
|
|
20
|
+
send(data: string | ArrayBuffer): void;
|
|
21
|
+
sendJson(msg: Record<string, unknown>): void;
|
|
22
|
+
isConnected(): boolean;
|
|
23
|
+
destroy(): void;
|
|
24
|
+
private cleanup;
|
|
25
|
+
private scheduleReconnect;
|
|
26
|
+
private startPing;
|
|
27
|
+
private stopPing;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=CallWebSocket.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CallWebSocket.d.ts","sourceRoot":"","sources":["../../src/core/CallWebSocket.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEhD,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,CAAC;AAC3D,MAAM,MAAM,eAAe,GAAG,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;AAC1D,MAAM,MAAM,kBAAkB,GAAG,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;AAM9D,qBAAa,aAAa;IACxB,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAwB;IACxC,OAAO,CAAC,WAAW,CAAwB;IAC3C,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,SAAS,CAAS;IAE1B,MAAM,EAAE,aAAa,CAAY;IACjC,QAAQ,EAAE,eAAe,CAAY;IACrC,kBAAkB,EAAE,kBAAkB,CAAY;gBAEtC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC;IAMhF,OAAO,CAAC,QAAQ;IASV,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAyD9B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAMtC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI5C,WAAW,IAAI,OAAO;IAItB,OAAO,IAAI,IAAI;IASf,OAAO,CAAC,OAAO;IAcf,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,QAAQ;CAMjB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type Listener = (...args: any[]) => void;
|
|
2
|
+
export declare class EventEmitter<Events extends {
|
|
3
|
+
[K in keyof Events]: Listener;
|
|
4
|
+
}> {
|
|
5
|
+
private listeners;
|
|
6
|
+
on<K extends keyof Events>(event: K, listener: Events[K]): this;
|
|
7
|
+
off<K extends keyof Events>(event: K, listener: Events[K]): this;
|
|
8
|
+
protected emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): void;
|
|
9
|
+
removeAllListeners(): void;
|
|
10
|
+
}
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=EventEmitter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventEmitter.d.ts","sourceRoot":"","sources":["../../src/core/EventEmitter.ts"],"names":[],"mappings":"AAAA,KAAK,QAAQ,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;AAEzC,qBAAa,YAAY,CAAC,MAAM,SAAS;KAAG,CAAC,IAAI,MAAM,MAAM,GAAG,QAAQ;CAAE;IACxE,OAAO,CAAC,SAAS,CAA0C;IAE3D,EAAE,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI;IAQ/D,GAAG,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI;IAKhE,SAAS,CAAC,IAAI,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;IAYtF,kBAAkB,IAAI,IAAI;CAG3B"}
|