voxnix 1.0.24 → 2.0.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 +345 -248
- package/dist/voxnix.iife.js +63 -422
- package/dist/voxnix.js +8161 -18045
- package/dist/voxnix.umd.cjs +62 -210
- package/package.json +4 -22
package/README.md
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
# Voxnix
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Headless voice SDK for the Omnix CPaaS platform. Voxnix connects the browser to your PBX over
|
|
4
|
+
SIP/WebSocket, manages the agent status (Ready / Not Ready / AUX)
|
|
5
|
+
through the CPaaS API, and gives you **functions + events** to build your own calling UI in any stack:
|
|
6
|
+
React, Vue, Angular, or plain HTML.
|
|
4
7
|
|
|
5
|
-
**Current version: `
|
|
8
|
+
**Current version: `2.0.0`**. Voxnix ships no UI. Upgrading from 1.x? See [Migrating from 1.x](#migrating-from-1x).
|
|
6
9
|
|
|
7
10
|
---
|
|
8
11
|
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
12
|
+
## Contents
|
|
13
|
+
|
|
14
|
+
- [Installation](#installation)
|
|
15
|
+
- [Quick start](#quick-start)
|
|
16
|
+
- [How it works](#how-it-works)
|
|
17
|
+
- [Config reference](#config-reference)
|
|
18
|
+
- [Instance API](#instance-api)
|
|
19
|
+
- [State](#state)
|
|
20
|
+
- [Events & callbacks](#events--callbacks)
|
|
21
|
+
- [Recipes](#recipes)
|
|
22
|
+
- [Browser requirements](#browser-requirements)
|
|
23
|
+
- [Security](#security)
|
|
24
|
+
- [Sample page](#sample-page)
|
|
25
|
+
- [Build](#build)
|
|
26
|
+
- [Migrating from 1.x](#migrating-from-1x)
|
|
27
|
+
- [Changelog](#changelog)
|
|
18
28
|
|
|
19
29
|
---
|
|
20
30
|
|
|
@@ -26,341 +36,428 @@ npm install voxnix
|
|
|
26
36
|
yarn add voxnix
|
|
27
37
|
```
|
|
28
38
|
|
|
39
|
+
Or use a script tag (no bundler):
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<script src="https://unpkg.com/voxnix/dist/voxnix.iife.js"></script>
|
|
43
|
+
<script>
|
|
44
|
+
const voice = OmnixWidget.init({ /* config */ });
|
|
45
|
+
</script>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| File | Format | Use |
|
|
49
|
+
|-------------------|--------|------------------------------|
|
|
50
|
+
| `voxnix.js` | ESM | `import` in bundlers |
|
|
51
|
+
| `voxnix.umd.cjs` | UMD | `require()` / legacy loaders |
|
|
52
|
+
| `voxnix.iife.js` | IIFE | `<script>` tag → `window.OmnixWidget` |
|
|
53
|
+
|
|
29
54
|
---
|
|
30
55
|
|
|
31
|
-
## Quick start
|
|
56
|
+
## Quick start
|
|
32
57
|
|
|
33
58
|
```js
|
|
34
59
|
import { OmnixWidget } from 'voxnix';
|
|
35
60
|
|
|
36
|
-
const
|
|
61
|
+
const voice = OmnixWidget.init({
|
|
37
62
|
auth: {
|
|
38
|
-
username: '1001',
|
|
39
|
-
pwd_pbx: 'secret',
|
|
40
|
-
pabx_host: 'sip.example.com',
|
|
41
|
-
port:
|
|
63
|
+
username: '1001', // SIP extension
|
|
64
|
+
pwd_pbx: 'secret', // SIP password
|
|
65
|
+
pabx_host: 'sip.example.com', // PBX host
|
|
66
|
+
port: 8089, // WSS port (default 8089)
|
|
42
67
|
},
|
|
68
|
+
'X-CPAAS-Key-Fingerprint': 'sfp_v1_...', // CPaaS API key (agent status)
|
|
69
|
+
cpaas_base_url: 'https://api-cpaas.omnix.co.id',
|
|
43
70
|
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
onRegisterStatus: (s) => console.log('register:', s),
|
|
72
|
+
onCallStatus: (s) => console.log('call:', s),
|
|
73
|
+
onIncomingCall: (call) => console.log('incoming from', call.number),
|
|
74
|
+
});
|
|
46
75
|
|
|
47
|
-
|
|
76
|
+
voice.dial('1002'); // outgoing call
|
|
77
|
+
voice.hangup(); // end it
|
|
78
|
+
```
|
|
48
79
|
|
|
49
|
-
|
|
50
|
-
autoReady: true, // auto-set agent to Ready after SIP register (default: true)
|
|
51
|
-
showAuxButton: false, // show AUX button in status bar (default: false)
|
|
52
|
-
auxOptions: [], // array of { code, label } — custom AUX codes
|
|
80
|
+
---
|
|
53
81
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
console.log('SIP status:', status);
|
|
58
|
-
},
|
|
59
|
-
onAgentStatus: (status) => console.log('Agent:', status), // 'READY' | 'NOT_READY' | 'AUX'
|
|
60
|
-
onIncomingCall: (data) => console.log('Incoming:', data),
|
|
61
|
-
onCallAnswered: (data) => console.log('Answered:', data),
|
|
62
|
-
onCallEnded: (data) => console.log('Ended:', data),
|
|
63
|
-
onCallUnanswered: () => console.log('Missed call'),
|
|
64
|
-
});
|
|
82
|
+
## How it works
|
|
83
|
+
|
|
84
|
+
### Registration flow
|
|
65
85
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
```
|
|
87
|
+
init()
|
|
88
|
+
└─ WebSocket connect wss://<sip_url | pabx_host>:<port>
|
|
89
|
+
└─ SIP REGISTER sip:<username>@<pbx_url | pabx_host>
|
|
90
|
+
├─ autoReady: true (default)
|
|
91
|
+
│ └─ POST Ready to CPaaS ──ok──► register_status 'REGISTERED' + agent_status 'READY'
|
|
92
|
+
│ └─fail─► register_status { status: 'FAILED', reason }
|
|
93
|
+
└─ autoReady: false
|
|
94
|
+
└─ GET extension status from CPaaS
|
|
95
|
+
├─ already Ready ──► 'REGISTERED' + 'READY'
|
|
96
|
+
└─ otherwise ──► 'SIP_CONNECTED' + 'NOT_READY' | 'AUX'
|
|
97
|
+
(call setReady() to go to 'REGISTERED')
|
|
76
98
|
```
|
|
77
99
|
|
|
78
|
-
|
|
100
|
+
Before every agent-status change, Voxnix checks the extension on CPaaS. If CPaaS reports
|
|
101
|
+
`is_register = 0`, Voxnix sends a SIP re-REGISTER and polls CPaaS (3 × 1s) until it reports
|
|
102
|
+
the extension as registered. If it still isn't registered, the status call returns `false`.
|
|
79
103
|
|
|
80
|
-
|
|
104
|
+
### `register_status` values
|
|
81
105
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
|
85
|
-
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `X-CPAAS-Key-Fingerprint` | `string` | — | **Required** for agent status API calls. See [section below](#x-cpaas-key-fingerprint) |
|
|
89
|
-
| `fingerprint` | `string` | — | Alias for `X-CPAAS-Key-Fingerprint` |
|
|
90
|
-
| `cpaasKeyFingerprint` | `string` | — | Alias for `X-CPAAS-Key-Fingerprint` |
|
|
91
|
-
| `placement` | `'bottom-left'` \| `'bottom-right'` \| `'top-left'` \| `'top-right'` | `'bottom-left'` | Fixed position of the floating widget |
|
|
92
|
-
| `offset` | `{ bottom?, top?, left?, right? }` | — | CSS pixel coords — overrides `placement` |
|
|
93
|
-
| `container` | `string` \| `HTMLElement` | — | Mount target. Auto-creates `<div id="omnix-widget-root">` if omitted |
|
|
94
|
-
| `autoReady` | `boolean` | `true` | Auto-call the Ready API right after SIP registration |
|
|
95
|
-
| `showAuxButton` | `boolean` | `false` | Show the AUX menu in the agent status bar |
|
|
96
|
-
| `auxOptions` | `Array<{ code: string, label: string }>` | `[]` | Custom AUX reason codes shown in the status dropdown |
|
|
97
|
-
| `onRegisterStatus` | `(status) => void` | — | `'REGISTERED'` or `{ status: 'FAILED', reason: string }` |
|
|
98
|
-
| `onAgentStatus` | `(status: string) => void` | — | `'READY'` \| `'NOT_READY'` \| `'AUX'` |
|
|
99
|
-
| `onIncomingCall` | `(data) => void` | — | Fired on incoming SIP INVITE |
|
|
100
|
-
| `onCallAnswered` | `(data) => void` | — | Fired when call is established |
|
|
101
|
-
| `onCallEnded` | `(data) => void` | — | Fired on BYE / hangup |
|
|
102
|
-
| `onCallUnanswered` | `() => void` | — | Fired when incoming call is not answered (missed) |
|
|
103
|
-
|
|
104
|
-
### `auth` object
|
|
105
|
-
|
|
106
|
-
| Field | Type | Description |
|
|
107
|
-
|-------------|----------|------------------------------------------|
|
|
108
|
-
| `username` | `string` | SIP username / extension number |
|
|
109
|
-
| `user_pbx` | `string` | Alias for `username` |
|
|
110
|
-
| `pwd_pbx` | `string` | SIP password |
|
|
111
|
-
| `secret` | `string` | Alias for `pwd_pbx` |
|
|
112
|
-
| `pabx_host` | `string` | SIP domain / WSS host (strips `http(s)://`) |
|
|
113
|
-
| `pbxurl` | `string` | Alias for `pabx_host` |
|
|
114
|
-
| `port` | `number` | WSS port (default `8089`) |
|
|
106
|
+
| Value | Meaning |
|
|
107
|
+
|-------------------------------|---------|
|
|
108
|
+
| `'CONNECTING'` | Initial state (only visible via `getState()`) |
|
|
109
|
+
| `'SIP_CONNECTED'` | SIP registered, agent not Ready yet (`autoReady: false`, or after `setNotReady()`) |
|
|
110
|
+
| `'REGISTERED'` | SIP registered **and** agent Ready. Calls can be made and received |
|
|
111
|
+
| `{ status: 'FAILED', reason }`| See reasons below |
|
|
115
112
|
|
|
116
|
-
|
|
113
|
+
Typical `reason` values:
|
|
117
114
|
|
|
118
|
-
|
|
115
|
+
- `Missing SIP credentials (username, password, host)`
|
|
116
|
+
- `WebSocket disconnected, retrying...`: Voxnix keeps reconnecting on its own. When the socket comes back, the registration flow runs again.
|
|
117
|
+
- `SIP registration failed: <cause>`: for example, a wrong password.
|
|
118
|
+
- `SIP Unregistered`
|
|
119
|
+
- `Failed to hit extension ready API`: the fingerprint is missing or wrong, or the CPaaS API rejected the request.
|
|
120
|
+
- `Extension not registered after SIP re-register`
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
### Call status flow
|
|
121
123
|
|
|
122
124
|
```
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
outgoing: Idle ─dial()─► Calling... ─answered─► InCall ─hangup / remote BYE─► Idle
|
|
126
|
+
└─rejected / no answer─► Idle (+ call_failed)
|
|
127
|
+
incoming: Idle ─INVITE─► Ringing ─answer()─► InCall ─► Idle
|
|
128
|
+
└─reject() / caller cancels─► Idle (+ callUnanswered)
|
|
125
129
|
```
|
|
126
130
|
|
|
127
|
-
|
|
131
|
+
Only one call at a time. A second incoming call while one is active is automatically rejected
|
|
132
|
+
with `486 Busy Here`.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Config reference
|
|
137
|
+
|
|
138
|
+
`OmnixWidget.init(config)`. There is one engine per page. Calling `init()` again destroys the previous instance first.
|
|
139
|
+
|
|
140
|
+
### SIP credentials: `auth`
|
|
141
|
+
|
|
142
|
+
| Field | Type | Required | Description |
|
|
143
|
+
|-------------|----------|----------|-------------|
|
|
144
|
+
| `username` | `string` | ✔ | SIP extension (alias: `user_pbx`) |
|
|
145
|
+
| `pwd_pbx` | `string` | ✔ | SIP password (alias: `secret`) |
|
|
146
|
+
| `pabx_host` | `string` | ✔* | PBX host (aliases: `pbxurl`, `host`). `http(s)://` is stripped |
|
|
147
|
+
| `port` | `number` | | WSS port, default `8089` |
|
|
128
148
|
|
|
129
|
-
|
|
149
|
+
\* Not required when `pbx_url` is given.
|
|
130
150
|
|
|
131
|
-
|
|
151
|
+
`pabxConfig: { auth: {...} }` is accepted as an alternative to `auth`.
|
|
132
152
|
|
|
133
|
-
|
|
134
|
-
|----------|------------|---------|
|
|
135
|
-
| 1 | `'X-CPAAS-Key-Fingerprint'` (top-level) | `{ 'X-CPAAS-Key-Fingerprint': 'abc123', auth: { ... } }` |
|
|
136
|
-
| 2 | `fingerprint` (top-level) | `{ fingerprint: 'abc123', auth: { ... } }` |
|
|
137
|
-
| 3 | `cpaasKeyFingerprint` (top-level) | `{ cpaasKeyFingerprint: 'abc123', auth: { ... } }` |
|
|
138
|
-
| 4 | `env.fingerprint` | `{ env: { fingerprint: 'abc123' }, auth: { ... } }` |
|
|
139
|
-
| 5 | `env.X_CPAAS_Key_Fingerprint` | `{ env: { X_CPAAS_Key_Fingerprint: 'abc123' }, auth: { ... } }` |
|
|
140
|
-
| 6 | `pabxConfig['X-CPAAS-Key-Fingerprint']` | `{ pabxConfig: { 'X-CPAAS-Key-Fingerprint': 'abc123', auth: { ... } } }` |
|
|
153
|
+
### Hosts & endpoints
|
|
141
154
|
|
|
142
|
-
|
|
155
|
+
| Option | Default | Description |
|
|
156
|
+
|------------------|----------------------------------|-------------|
|
|
157
|
+
| `pbx_url` | `auth.pabx_host` | SIP domain used in the SIP URI `sip:<username>@<pbx_url>` (alias: `pabxConfig.pbx_url`) |
|
|
158
|
+
| `sip_url` | SIP domain | WebSocket host: `wss://<sip_url>:<port>`. `ws(s)://` and a trailing `/` are stripped (alias: `pabxConfig.sip_url`) |
|
|
159
|
+
| `cpaas_base_url` | `https://api-cpaas.omnix.co.id` | CPaaS API base URL, must be `https://`. A trailing `/` is stripped (aliases: `cpaasBaseUrl`, `env.CPAAS_BASE_URL`, `pabxConfig.cpaas_base_url`) |
|
|
160
|
+
|
|
161
|
+
Use these when the SIP domain, the WebSocket host and the API live on different hosts:
|
|
143
162
|
|
|
144
163
|
```js
|
|
145
|
-
// Option A — top-level key (simplest)
|
|
146
164
|
OmnixWidget.init({
|
|
147
|
-
'
|
|
148
|
-
|
|
149
|
-
|
|
165
|
+
auth: { username: '1001', pwd_pbx: '***', pabx_host: 'sip.example.com' },
|
|
166
|
+
'X-CPAAS-Key-Fingerprint': 'sfp_v1_...',
|
|
167
|
+
cpaas_base_url: 'https://api.example.com/',
|
|
168
|
+
pbx_url: 'pbx.example.com', // SIP domain
|
|
169
|
+
sip_url: 'wss.example.com', // WSS host
|
|
150
170
|
});
|
|
171
|
+
```
|
|
151
172
|
|
|
152
|
-
|
|
153
|
-
OmnixWidget.init({
|
|
154
|
-
pabxConfig: {
|
|
155
|
-
'X-CPAAS-Key-Fingerprint': 'your-fingerprint-here',
|
|
156
|
-
auth: { username: '1001', pwd_pbx: 'secret', pabx_host: 'sip.example.com' },
|
|
157
|
-
},
|
|
158
|
-
});
|
|
173
|
+
### `X-CPAAS-Key-Fingerprint`
|
|
159
174
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
175
|
+
The API key Voxnix sends as a header to the CPaaS endpoints:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
GET {cpaas_base_url}/voice/portal/extension?extension=<username> → read status / is_register
|
|
179
|
+
POST {cpaas_base_url}/voice/extensions/status → { extension, status, reason }
|
|
165
180
|
```
|
|
166
181
|
|
|
167
|
-
|
|
182
|
+
Voxnix uses the first of these keys that is present:
|
|
183
|
+
|
|
184
|
+
| Priority | Key |
|
|
185
|
+
|----------|-----|
|
|
186
|
+
| 1 | `'X-CPAAS-Key-Fingerprint'` |
|
|
187
|
+
| 2 | `fingerprint` |
|
|
188
|
+
| 3 | `cpaasKeyFingerprint` |
|
|
189
|
+
| 4 | `env.fingerprint` |
|
|
190
|
+
| 5 | `env.X_CPAAS_Key_Fingerprint` |
|
|
191
|
+
| 6 | `pabxConfig['X-CPAAS-Key-Fingerprint']` |
|
|
192
|
+
|
|
193
|
+
> Without a fingerprint, every agent-status call returns `false`. With `autoReady: true` the
|
|
194
|
+
> engine then reports `FAILED` ("Failed to hit extension ready API") even though SIP registered fine.
|
|
195
|
+
|
|
196
|
+
### Behaviour
|
|
197
|
+
|
|
198
|
+
| Option | Type | Default | Description |
|
|
199
|
+
|-------------|--------------------------|---------|-------------|
|
|
200
|
+
| `autoReady` | `boolean` | `true` | Set the agent Ready right after SIP registration. If `false`, the engine restores the current CPaaS status and waits for `setReady()` |
|
|
201
|
+
| `ringtone` | `boolean` | `true` | Play the built-in ringtone on an incoming call. Set `false` to play your own |
|
|
202
|
+
| `contacts` | `{ [number]: string }` | — | Name lookup used for `name` when the caller sends no display name |
|
|
203
|
+
|
|
204
|
+
### Callbacks
|
|
205
|
+
|
|
206
|
+
All callbacks are optional. Payloads are described in [Events & callbacks](#events--callbacks).
|
|
207
|
+
|
|
208
|
+
| Option | Called when |
|
|
209
|
+
|--------------------|-------------|
|
|
210
|
+
| `onRegisterStatus` | Registration becomes `'REGISTERED'` or `FAILED`. `SIP_CONNECTED` is **not** reported here; use `on('register_status')` to see it |
|
|
211
|
+
| `onAgentStatus` | Agent status changes |
|
|
212
|
+
| `onCallStatus` | Call status changes |
|
|
213
|
+
| `onCallFailed` | An outgoing call fails (rejected, busy, unreachable, …) |
|
|
214
|
+
| `onIncomingCall` | An incoming call starts ringing |
|
|
215
|
+
| `onCallAnswered` | A call (either direction) is connected |
|
|
216
|
+
| `onCallEnded` | A connected call ends |
|
|
217
|
+
| `onCallUnanswered` | An incoming call ends without being answered (missed, rejected, caller cancelled) |
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Instance API
|
|
168
222
|
|
|
169
|
-
|
|
223
|
+
`init()` returns:
|
|
224
|
+
|
|
225
|
+
| Method | Returns | Description |
|
|
226
|
+
|--------|---------|-------------|
|
|
227
|
+
| `dial(number)` | — | Start an outgoing call to `sip:<number>@<SIP domain>`. Ignored if another call is active. Invalid characters → `call_failed` `'Invalid number'` |
|
|
228
|
+
| `answer()` | — | Answer the ringing incoming call |
|
|
229
|
+
| `reject()` | — | Reject the ringing incoming call (same as `hangup()`) |
|
|
230
|
+
| `hangup()` | — | End the current call. Sends CANCEL while dialing, rejects while ringing, BYE when connected |
|
|
231
|
+
| `mute(muted = true)` | — | `true` mutes the microphone, `false` unmutes it. Only during a connected call |
|
|
232
|
+
| `hold(onHold = true)` | — | `true` holds the call, `false` resumes it. Only during a connected call |
|
|
233
|
+
| `sendDtmf(tones)` | — | Send DTMF during a connected call. Accepts one tone or a string: `'5'`, `'123#'` |
|
|
234
|
+
| `setReady()` | `Promise<boolean>` | Agent → Ready. Emits `'REGISTERED'` + `'READY'` on success |
|
|
235
|
+
| `setNotReady()` | `Promise<boolean>` | Agent → Not Ready (CPaaS `Logout`). Emits `'SIP_CONNECTED'` + `'NOT_READY'` |
|
|
236
|
+
| `setAux(code = 'AUX', label = code)` | `Promise<boolean>` | Agent → AUX. `code` is sent to CPaaS as the reason; `label` is for your UI |
|
|
237
|
+
| `getState()` | `object` | Snapshot of the current state, see [State](#state) |
|
|
238
|
+
| `on(event, cb)` | `() => void` | Subscribe to an event. Returns an unsubscribe function |
|
|
239
|
+
| `destroy()` | — | End any call, unregister, close the WebSocket, and drop all listeners |
|
|
240
|
+
|
|
241
|
+
The call controls are safe to call at any time. Outside a valid state they do nothing.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## State
|
|
246
|
+
|
|
247
|
+
`getState()` returns a fresh copy each time:
|
|
170
248
|
|
|
171
249
|
```js
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
// ...
|
|
177
|
-
|
|
250
|
+
{
|
|
251
|
+
registerStatus: 'REGISTERED', // 'CONNECTING' | 'SIP_CONNECTED' | 'REGISTERED' | 'FAILED'
|
|
252
|
+
agentStatus: 'READY', // 'NOT_READY' | 'READY' | 'AUX'
|
|
253
|
+
currentAux: null, // { code, label } while AUX
|
|
254
|
+
callStatus: 'InCall', // 'Idle' | 'Calling...' | 'Ringing' | 'InCall'
|
|
255
|
+
callDuration: '01:23', // 'MM:SS' or 'HH:MM:SS'
|
|
256
|
+
isMuted: false,
|
|
257
|
+
isOnHold: false,
|
|
258
|
+
remote: { number: '1002', name: 'Jane', direction: 'outgoing' }, // null when Idle
|
|
259
|
+
}
|
|
178
260
|
```
|
|
179
261
|
|
|
262
|
+
Use it to draw your UI when it first mounts, then keep it in sync with events.
|
|
263
|
+
|
|
180
264
|
---
|
|
181
265
|
|
|
182
|
-
##
|
|
266
|
+
## Events & callbacks
|
|
183
267
|
|
|
184
|
-
`
|
|
268
|
+
Subscribe with `voice.on(event, cb)`. Events that have a callback option fire that callback too.
|
|
185
269
|
|
|
186
|
-
|
|
|
187
|
-
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
270
|
+
| Event | Payload | Callback |
|
|
271
|
+
|------------------------|---------|----------|
|
|
272
|
+
| `register_status` | `'SIP_CONNECTED'` \| `'REGISTERED'` \| `{ status: 'FAILED', reason }` | `onRegisterStatus` (skips `SIP_CONNECTED`) |
|
|
273
|
+
| `agent_status` | `'READY'` \| `'NOT_READY'` \| `'AUX'` (`getState().currentAux` holds the AUX code) | `onAgentStatus` |
|
|
274
|
+
| `call_status` | `'Idle'` \| `'Calling...'` \| `'Ringing'` \| `'InCall'` | `onCallStatus` |
|
|
275
|
+
| `call_duration` | `'00:42'`, every second while connected, `'00:00'` on end | — |
|
|
276
|
+
| `call_failed` | `cause` string, e.g. `'Busy'`, `'Rejected'`, `'Unavailable'` | `onCallFailed` |
|
|
277
|
+
| `incoming_call` | `{ number, name, direction: 'incoming' }` | `onIncomingCall` |
|
|
278
|
+
| `sync_call_answered` | `{ number, name, direction }` | `onCallAnswered` |
|
|
279
|
+
| `sync_call_ended` | `{ number, name, direction, cause, originator }` | `onCallEnded` |
|
|
280
|
+
| `sync_call_unanswered` | `{ number, name, direction, cause, originator }` | `onCallUnanswered` |
|
|
281
|
+
| `mute_status` | `boolean` | — |
|
|
282
|
+
| `hold_status` | `boolean` | — |
|
|
283
|
+
|
|
284
|
+
- `direction`: `'incoming'` or `'outgoing'`.
|
|
285
|
+
- `originator`: who ended the call. `'local'` means you, `'remote'` means the other side, `'system'` means a network or SIP error.
|
|
286
|
+
- `cause`: the end reason, such as `'Terminated'`, `'Canceled'`, `'Rejected'`, `'Busy'` or `'No Answer'`.
|
|
195
287
|
|
|
196
288
|
---
|
|
197
289
|
|
|
198
|
-
##
|
|
290
|
+
## Recipes
|
|
199
291
|
|
|
200
|
-
|
|
292
|
+
### Outgoing call
|
|
201
293
|
|
|
202
|
-
|
|
294
|
+
```js
|
|
295
|
+
voice.on('call_status', (s) => render(s));
|
|
296
|
+
voice.on('call_duration', (d) => (timer.textContent = d));
|
|
297
|
+
voice.on('call_failed', (cause) => toast(`Call failed: ${cause}`));
|
|
298
|
+
|
|
299
|
+
callBtn.onclick = () => voice.dial(input.value);
|
|
300
|
+
hangupBtn.onclick = () => voice.hangup();
|
|
301
|
+
muteBtn.onclick = () => voice.mute(!voice.getState().isMuted);
|
|
302
|
+
holdBtn.onclick = () => voice.hold(!voice.getState().isOnHold);
|
|
303
|
+
keypad.onclick = (e) => voice.sendDtmf(e.target.dataset.key);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Incoming call
|
|
203
307
|
|
|
204
308
|
```js
|
|
205
|
-
OmnixWidget.init({
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
{ code: 'BRK', label: 'Break' },
|
|
212
|
-
{ code: 'LCH', label: 'Lunch' },
|
|
213
|
-
{ code: 'TRN', label: 'Training' },
|
|
214
|
-
],
|
|
215
|
-
onAgentStatus: (status) => console.log('Agent status changed:', status),
|
|
309
|
+
const voice = OmnixWidget.init({
|
|
310
|
+
...config,
|
|
311
|
+
onIncomingCall: ({ number, name }) => showRinging(name || number),
|
|
312
|
+
onCallAnswered: () => showInCall(),
|
|
313
|
+
onCallEnded: ({ cause }) => showIdle(),
|
|
314
|
+
onCallUnanswered: ({ number }) => addMissedCall(number),
|
|
216
315
|
});
|
|
316
|
+
|
|
317
|
+
answerBtn.onclick = () => voice.answer();
|
|
318
|
+
rejectBtn.onclick = () => voice.reject();
|
|
217
319
|
```
|
|
218
320
|
|
|
219
|
-
|
|
220
|
-
|-------------|--------------|--------------------------------------|
|
|
221
|
-
| `READY` | 🟢 Green | Agent is available for calls |
|
|
222
|
-
| `NOT_READY` | ⚪ Gray | Agent is unavailable |
|
|
223
|
-
| `AUX` | 🟡 Amber | Agent is on a break / custom reason |
|
|
321
|
+
### Agent status (manual Ready)
|
|
224
322
|
|
|
225
|
-
|
|
323
|
+
```js
|
|
324
|
+
const voice = OmnixWidget.init({ ...config, autoReady: false });
|
|
226
325
|
|
|
227
|
-
|
|
326
|
+
voice.on('register_status', (s) => {
|
|
327
|
+
if (s === 'SIP_CONNECTED') showSetReadyButton(); // connected, agent not Ready
|
|
328
|
+
if (s === 'REGISTERED') showDialer();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
readyBtn.onclick = async () => { if (!(await voice.setReady())) toast('Failed to set Ready'); };
|
|
332
|
+
notReadyBtn.onclick = () => voice.setNotReady();
|
|
333
|
+
breakBtn.onclick = () => voice.setAux('BRK', 'Break');
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### React hook
|
|
228
337
|
|
|
229
338
|
```jsx
|
|
230
|
-
import { useEffect, useRef } from 'react';
|
|
339
|
+
import { useEffect, useRef, useState } from 'react';
|
|
231
340
|
import { OmnixWidget } from 'voxnix';
|
|
232
341
|
|
|
233
|
-
function
|
|
234
|
-
const
|
|
342
|
+
export function useVoice(config) {
|
|
343
|
+
const ref = useRef(null);
|
|
344
|
+
const [state, setState] = useState(null);
|
|
235
345
|
|
|
236
346
|
useEffect(() => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
auxOptions: [{ code: 'BRK', label: 'Break' }],
|
|
248
|
-
|
|
249
|
-
onIncomingCall: (data) => { /* navigate, show notification, etc. */ },
|
|
250
|
-
onCallAnswered: (data) => { /* routing logic */ },
|
|
251
|
-
onCallEnded: () => { /* cleanup */ },
|
|
252
|
-
onRegisterStatus: (s) => { /* update UI */ },
|
|
253
|
-
onAgentStatus: (s) => { /* s: 'READY' | 'NOT_READY' | 'AUX' */ },
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
return () => widgetRef.current?.destroy();
|
|
257
|
-
}, []);
|
|
258
|
-
|
|
259
|
-
return <div id="app">...</div>;
|
|
347
|
+
const voice = OmnixWidget.init(config);
|
|
348
|
+
ref.current = voice;
|
|
349
|
+
const sync = () => setState(voice.getState());
|
|
350
|
+
const events = ['register_status', 'agent_status', 'call_status', 'call_duration', 'mute_status', 'hold_status'];
|
|
351
|
+
events.forEach((e) => voice.on(e, sync));
|
|
352
|
+
sync();
|
|
353
|
+
return () => voice.destroy();
|
|
354
|
+
}, []); // init once
|
|
355
|
+
|
|
356
|
+
return [state, ref];
|
|
260
357
|
}
|
|
261
|
-
```
|
|
262
358
|
|
|
263
|
-
|
|
359
|
+
// const [state, voice] = useVoice(config);
|
|
360
|
+
// <button onClick={() => voice.current.dial('1002')}>Call</button>
|
|
361
|
+
```
|
|
264
362
|
|
|
265
|
-
|
|
363
|
+
### Vue 3
|
|
266
364
|
|
|
267
365
|
```js
|
|
366
|
+
import { onMounted, onUnmounted, ref } from 'vue';
|
|
268
367
|
import { OmnixWidget } from 'voxnix';
|
|
269
368
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
369
|
+
export function useVoice(config) {
|
|
370
|
+
const state = ref(null);
|
|
371
|
+
let voice;
|
|
372
|
+
onMounted(() => {
|
|
373
|
+
voice = OmnixWidget.init(config);
|
|
374
|
+
const sync = () => (state.value = voice.getState());
|
|
375
|
+
['register_status', 'agent_status', 'call_status', 'call_duration', 'mute_status', 'hold_status']
|
|
376
|
+
.forEach((e) => voice.on(e, sync));
|
|
377
|
+
sync();
|
|
278
378
|
});
|
|
279
|
-
onUnmounted(() =>
|
|
280
|
-
}
|
|
379
|
+
onUnmounted(() => voice?.destroy());
|
|
380
|
+
return { state, dial: (n) => voice.dial(n), hangup: () => voice.hangup() };
|
|
381
|
+
}
|
|
281
382
|
```
|
|
282
383
|
|
|
283
384
|
---
|
|
284
385
|
|
|
285
|
-
##
|
|
386
|
+
## Browser requirements
|
|
286
387
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
OmnixWidget.init({
|
|
292
|
-
auth: {
|
|
293
|
-
username: '1001',
|
|
294
|
-
pwd_pbx: 'secret',
|
|
295
|
-
pabx_host: 'sip.example.com',
|
|
296
|
-
},
|
|
297
|
-
'X-CPAAS-Key-Fingerprint': 'your-fingerprint-here',
|
|
298
|
-
placement: 'bottom-right',
|
|
299
|
-
autoReady: true,
|
|
300
|
-
showAuxButton: true,
|
|
301
|
-
auxOptions: [
|
|
302
|
-
{ code: 'BRK', label: 'Break' },
|
|
303
|
-
{ code: 'LCH', label: 'Lunch' },
|
|
304
|
-
],
|
|
305
|
-
onRegisterStatus: (s) => console.log('SIP:', s),
|
|
306
|
-
onIncomingCall: (d) => console.log('Incoming:', d),
|
|
307
|
-
onAgentStatus: (s) => console.log('Agent:', s),
|
|
308
|
-
});
|
|
309
|
-
</script>
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
> If you need to use your own React instance, include `react@18` and `react-dom@18` UMD builds **before** the script tag and use `dist/voxnix.js` (ESM) or `dist/voxnix.umd.cjs` (CJS) instead.
|
|
388
|
+
- **HTTPS (or `localhost`)**: browsers only give microphone access on secure origins.
|
|
389
|
+
- **Microphone permission**: the browser asks for it on the first `dial()` or `answer()`. If the user denies it, the call fails (`call_failed`, or `onCallUnanswered` for an incoming call).
|
|
390
|
+
- **Remote audio**: Voxnix creates a hidden `<audio id="voxnix-remote-audio">` in `document.body`. Autoplay rules may block it until the user has clicked something on the page. Answering or dialing from a click handler counts as that interaction.
|
|
391
|
+
- **Ringtone**: generated with WebAudio, which has the same user-interaction rule. Set `ringtone: false` to use your own sound.
|
|
313
392
|
|
|
314
393
|
---
|
|
315
394
|
|
|
316
|
-
##
|
|
317
|
-
|
|
318
|
-
These shortcuts are active whenever the dialer widget is open:
|
|
395
|
+
## Security
|
|
319
396
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
397
|
+
- **Treat caller data as untrusted.** `name` comes from the caller's SIP display name and `number`
|
|
398
|
+
from their SIP URI. Render them with `textContent` (or your framework's default escaping),
|
|
399
|
+
never `innerHTML`.
|
|
400
|
+
- **Credentials live in the browser.** The SIP password and `X-CPAAS-Key-Fingerprint` are readable
|
|
401
|
+
by anyone with devtools on that page. Fetch them from your backend after the agent logs in,
|
|
402
|
+
use per-agent SIP credentials, and never hardcode them in frontend code.
|
|
403
|
+
- **HTTPS only.** `cpaas_base_url` must be `https://` (`http://localhost` is allowed for development).
|
|
404
|
+
Any other URL disables CPaaS API calls, and agent-status methods return `false`.
|
|
405
|
+
- **Input validation.** `dial()` only accepts `0-9 A-Z a-z + * # . _ -`. Anything else emits
|
|
406
|
+
`call_failed` with `'Invalid number'`.
|
|
407
|
+
- Voxnix never logs credentials or API responses to the console.
|
|
326
408
|
|
|
327
409
|
---
|
|
328
410
|
|
|
329
|
-
##
|
|
330
|
-
|
|
331
|
-
```bash
|
|
332
|
-
# ESM + UMD (peer React — for npm consumers)
|
|
333
|
-
yarn build:lib
|
|
411
|
+
## Sample page
|
|
334
412
|
|
|
335
|
-
|
|
336
|
-
|
|
413
|
+
[`examples/index.html`](examples/index.html) is a plain-HTML page that loads the script-tag
|
|
414
|
+
build. It calls `OmnixWidget.init()` with a config object, has buttons for every API method, and
|
|
415
|
+
shows a live `getState()` view and an event log.
|
|
337
416
|
|
|
338
|
-
|
|
339
|
-
|
|
417
|
+
```bash
|
|
418
|
+
npm run build
|
|
419
|
+
python3 -m http.server 5599 # from the repo root
|
|
420
|
+
# open http://localhost:5599/examples/
|
|
340
421
|
```
|
|
341
422
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
| File | Format | React | Use case |
|
|
345
|
-
|--------------------|--------|----------|-----------------------|
|
|
346
|
-
| `voxnix.js` | ESM | peer dep | `import` / bundlers |
|
|
347
|
-
| `voxnix.umd.cjs` | UMD | peer dep | CommonJS / `require` |
|
|
348
|
-
| `voxnix.iife.js` | IIFE | bundled | `<script>` / CDN |
|
|
423
|
+
Edit the `CONFIG` object at the top of the script to point it at your PBX.
|
|
349
424
|
|
|
350
425
|
---
|
|
351
426
|
|
|
352
|
-
##
|
|
427
|
+
## Build
|
|
353
428
|
|
|
354
429
|
```bash
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
430
|
+
npm install
|
|
431
|
+
npm run build # ESM + UMD + IIFE into dist/
|
|
432
|
+
npm run build:lib # ESM + UMD only
|
|
433
|
+
npm run build:iife # IIFE only
|
|
434
|
+
npm run watch # rebuild ESM + UMD on change
|
|
358
435
|
```
|
|
359
436
|
|
|
360
437
|
---
|
|
361
438
|
|
|
439
|
+
## Migrating from 1.x
|
|
440
|
+
|
|
441
|
+
- **The UI is gone.** There is no floating button or dialer panel. The options `placement`, `offset`, `container`, `showAuxButton` and `auxOptions` are ignored. Build the UI with the [Instance API](#instance-api) and [events](#events--callbacks).
|
|
442
|
+
- **New SIP engine.** Call callbacks now receive `{ number, name, direction, ... }` instead of a raw SIP session object.
|
|
443
|
+
- `hold(true)` now holds and `hold(false)` resumes (1.x had them reversed).
|
|
444
|
+
- `onCallEnded` fires once per call. Before, it could fire twice for incoming calls.
|
|
445
|
+
- New in 2.0: `sendDtmf`, `setReady`, `setNotReady`, `setAux`, `getState`, `on`, plus the options `onCallStatus`, `onCallFailed`, `ringtone` and `contacts`.
|
|
446
|
+
- `contacts` has no built-in demo entries any more. Pass your own map.
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
362
450
|
## Changelog
|
|
363
451
|
|
|
452
|
+
### v2.0.0
|
|
453
|
+
- Removed the built-in React UI; Voxnix is now a headless SDK (no React / Tailwind dependencies)
|
|
454
|
+
- New internal SIP engine
|
|
455
|
+
- New API: `sendDtmf`, `setReady`, `setNotReady`, `setAux`, `getState`, `on`; options `onCallStatus`, `onCallFailed`, `ringtone`, `contacts`
|
|
456
|
+
- Configurable `cpaas_base_url`, `pbx_url` (SIP domain), `sip_url` (WSS host)
|
|
457
|
+
- Re-REGISTER + CPaaS poll when the extension reports `is_register = 0`
|
|
458
|
+
- Security: credentials and API responses are no longer logged (1.0.23–1.0.24 logged the full config); HTTPS-only CPaaS URL; `dial()` input validation; `prepublishOnly` build
|
|
459
|
+
- Fixes: cancel while dialing, double `onCallEnded`, inverted `hold()`, outgoing call duration and callbacks, busy second incoming call, `destroy()` not stopping SIP, IIFE global `OmnixWidget.init`
|
|
460
|
+
|
|
364
461
|
### v1.0.12
|
|
365
462
|
- Rollback DevToast / console interceptor feature
|
|
366
463
|
|