@digitalsac/digicalls-sdk 0.1.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/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/client.d.ts +22 -0
- package/dist/digicalls.js +300 -0
- package/dist/digicalls.js.map +1 -0
- package/dist/digicalls.umd.cjs +2 -0
- package/dist/digicalls.umd.cjs.map +1 -0
- package/dist/index.d.ts +132 -0
- package/dist/webrtc.d.ts +7 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DigitalSac
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @digitalsac/digicalls-sdk
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@digitalsac/digicalls-sdk)
|
|
4
|
+
[](https://www.npmjs.com/package/@digitalsac/digicalls-sdk)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
SDK (motor) do **DigiCalls** — chamadas de WhatsApp por WebRTC, servidas pelo **seu**
|
|
8
|
+
backend DigiCalls. A UI pronta (webphone) está em **@digitalsac/digicalls-widget**.
|
|
9
|
+
|
|
10
|
+
## Instalação
|
|
11
|
+
```bash
|
|
12
|
+
npm i @digitalsac/digicalls-sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Uso
|
|
16
|
+
```ts
|
|
17
|
+
import { DigiCalls } from "@digitalsac/digicalls-sdk";
|
|
18
|
+
|
|
19
|
+
const dc = new DigiCalls({
|
|
20
|
+
baseUrl: "https://calls.digitalsac.io",
|
|
21
|
+
tokens: [linha1, linha2], // device tokens (dev_…), 1 por linha
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
dc.onOffer((offer) => offer.accept()); // receber
|
|
25
|
+
const { call, err } = await dc.startCall({ fromTokens: [linha1], to: "5511999998888" });
|
|
26
|
+
call?.onPeerAccept((c) => console.log("atendida", c.callId));
|
|
27
|
+
call?.onEnd(({ reason }) => console.log("encerrada", reason));
|
|
28
|
+
// call.mute() / call.unmute() / call.end()
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## API
|
|
32
|
+
- `new DigiCalls({ baseUrl, tokens, deviceNames? })`
|
|
33
|
+
- `onOffer(cb)` · `onPeerInfo(cb)` · `onUpdate(cb)` · `getDevices()` · `addDevices()` · `removeDevices()`
|
|
34
|
+
- `startCall({ fromTokens, to })` → `{ call, err }`
|
|
35
|
+
- `getMultimediaDevices()` · `setMic(id)` · `setOutput(id)`
|
|
36
|
+
|
|
37
|
+
O **device token** (`dev_…`) de cada linha é criado no DigiCalls via `POST /api/devices`
|
|
38
|
+
(com a API key do usuário, header `X-Api-Key`).
|
|
39
|
+
|
|
40
|
+
## Build
|
|
41
|
+
```bash
|
|
42
|
+
npm run build # dist/ (ES + UMD + .d.ts)
|
|
43
|
+
```
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type BrokerEvent = {
|
|
2
|
+
type: string;
|
|
3
|
+
sessionId?: string;
|
|
4
|
+
id?: string;
|
|
5
|
+
peer?: string;
|
|
6
|
+
status?: string;
|
|
7
|
+
reason?: string;
|
|
8
|
+
[k: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
export declare class Client {
|
|
11
|
+
readonly baseUrl: string;
|
|
12
|
+
readonly token: string;
|
|
13
|
+
readonly clientId: string;
|
|
14
|
+
private iceCache;
|
|
15
|
+
constructor(baseUrl: string, token: string, clientId?: string);
|
|
16
|
+
private headers;
|
|
17
|
+
get<T>(path: string): Promise<T>;
|
|
18
|
+
post<T>(path: string, body?: unknown): Promise<T>;
|
|
19
|
+
del(path: string): Promise<void>;
|
|
20
|
+
events(onEvent: (ev: BrokerEvent) => void): EventSource;
|
|
21
|
+
iceServers(): Promise<RTCIceServer[]>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
const p = [{ urls: "stun:stun.l.google.com:19302" }];
|
|
2
|
+
class g {
|
|
3
|
+
constructor(e, t, s) {
|
|
4
|
+
this.baseUrl = e, this.token = t, this.iceCache = null, this.baseUrl = e.replace(/\/+$/, ""), this.clientId = s ?? "dc_" + Math.random().toString(36).slice(2, 12);
|
|
5
|
+
}
|
|
6
|
+
headers() {
|
|
7
|
+
return {
|
|
8
|
+
"Content-Type": "application/json",
|
|
9
|
+
Authorization: "Bearer " + this.token,
|
|
10
|
+
"X-Client-Id": this.clientId
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async get(e) {
|
|
14
|
+
const t = await fetch(this.baseUrl + e, { headers: this.headers() });
|
|
15
|
+
if (!t.ok) throw new Error(`${e} ${t.status}`);
|
|
16
|
+
return await t.json();
|
|
17
|
+
}
|
|
18
|
+
async post(e, t = {}) {
|
|
19
|
+
const s = await fetch(this.baseUrl + e, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: this.headers(),
|
|
22
|
+
body: JSON.stringify(t)
|
|
23
|
+
});
|
|
24
|
+
if (!s.ok) throw new Error(`${e} ${s.status}`);
|
|
25
|
+
if (s.status !== 204)
|
|
26
|
+
return await s.json();
|
|
27
|
+
}
|
|
28
|
+
async del(e) {
|
|
29
|
+
const t = await fetch(this.baseUrl + e, { method: "DELETE", headers: this.headers() });
|
|
30
|
+
if (!t.ok && t.status !== 204) throw new Error(`${e} ${t.status}`);
|
|
31
|
+
}
|
|
32
|
+
// events opens the SSE stream for this device token. EventSource can't send
|
|
33
|
+
// headers, so the token goes in the query (the backend reads ?token=).
|
|
34
|
+
events(e) {
|
|
35
|
+
const t = new URL(this.baseUrl + "/api/events");
|
|
36
|
+
t.searchParams.set("token", this.token), t.searchParams.set("clientId", this.clientId);
|
|
37
|
+
const s = new EventSource(t.toString());
|
|
38
|
+
return s.onmessage = (i) => {
|
|
39
|
+
try {
|
|
40
|
+
e(JSON.parse(i.data));
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}, s;
|
|
44
|
+
}
|
|
45
|
+
async iceServers() {
|
|
46
|
+
if (this.iceCache) return this.iceCache;
|
|
47
|
+
try {
|
|
48
|
+
const { iceServers: e } = await this.get("/api/webrtc-config");
|
|
49
|
+
this.iceCache = e?.length ? e : p;
|
|
50
|
+
} catch {
|
|
51
|
+
this.iceCache = p;
|
|
52
|
+
}
|
|
53
|
+
return this.iceCache;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const v = (c, e = 2e3) => new Promise((t) => {
|
|
57
|
+
if (c.iceGatheringState === "complete") return t();
|
|
58
|
+
const s = () => {
|
|
59
|
+
clearTimeout(r), c.removeEventListener("icegatheringstatechange", i), t();
|
|
60
|
+
}, i = () => c.iceGatheringState === "complete" && s(), r = setTimeout(s, e);
|
|
61
|
+
c.addEventListener("icegatheringstatechange", i);
|
|
62
|
+
});
|
|
63
|
+
async function f(c, e, t, s, i, r) {
|
|
64
|
+
const a = new RTCPeerConnection({ iceServers: await c.iceServers() }), d = a.addTransceiver("audio", { direction: "sendonly" });
|
|
65
|
+
if (s) {
|
|
66
|
+
const h = s.getAudioTracks()[0];
|
|
67
|
+
h && await d.sender.replaceTrack(h);
|
|
68
|
+
}
|
|
69
|
+
a.addTransceiver("audio", { direction: "recvonly" });
|
|
70
|
+
const o = new MediaStream(), n = typeof document < "u" ? document.createElement("audio") : null;
|
|
71
|
+
n && (n.autoplay = !0, n.playsInline = !0, n.srcObject = o), a.ontrack = (h) => {
|
|
72
|
+
const u = h.streams[0];
|
|
73
|
+
if (u ? u.getTracks().forEach((l) => o.addTrack(l)) : o.addTrack(h.track), n) {
|
|
74
|
+
n.srcObject = o;
|
|
75
|
+
const l = n;
|
|
76
|
+
i && l.setSinkId && l.setSinkId(i).catch(() => {
|
|
77
|
+
}), n.play().catch(() => {
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}, a.onconnectionstatechange = () => {
|
|
81
|
+
["failed", "closed", "disconnected"].includes(a.connectionState) && r();
|
|
82
|
+
}, await a.setLocalDescription(await a.createOffer()), await v(a);
|
|
83
|
+
const { sdp_answer: I } = await c.post(
|
|
84
|
+
`/api/sessions/${e}/calls/${t}/webrtc`,
|
|
85
|
+
{ sdp_offer: a.localDescription.sdp }
|
|
86
|
+
);
|
|
87
|
+
return await a.setRemoteDescription({ type: "answer", sdp: I }), { pc: a, remote: o, close: () => {
|
|
88
|
+
try {
|
|
89
|
+
a.getSenders().forEach((h) => h.track?.stop()), a.close();
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
n && (n.srcObject = null, n.remove());
|
|
93
|
+
} };
|
|
94
|
+
}
|
|
95
|
+
function b(c) {
|
|
96
|
+
return c ? c.split("@")[0].replace(/[^0-9]/g, "") : "";
|
|
97
|
+
}
|
|
98
|
+
async function m(c) {
|
|
99
|
+
return navigator.mediaDevices.getUserMedia({
|
|
100
|
+
audio: c ? { deviceId: { exact: c } } : !0
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
class w {
|
|
104
|
+
constructor(e, t, s, i, r) {
|
|
105
|
+
this.client = e, this.sessionId = t, this.callId = s, this.peer = i, this.direction = r, this.media = null, this.mic = null, this.accepted = !1, this.ended = !1, this.cbAccept = [], this.cbEnd = [], this.cbUnanswered = [], this.cbState = [];
|
|
106
|
+
}
|
|
107
|
+
onPeerAccept(e) {
|
|
108
|
+
return this.cbAccept.push(e), this.accepted && e(this), this;
|
|
109
|
+
}
|
|
110
|
+
onEnd(e) {
|
|
111
|
+
return this.cbEnd.push(e), this;
|
|
112
|
+
}
|
|
113
|
+
onUnanswered(e) {
|
|
114
|
+
return this.cbUnanswered.push(e), this;
|
|
115
|
+
}
|
|
116
|
+
onStateChange(e) {
|
|
117
|
+
return this.cbState.push(e), this;
|
|
118
|
+
}
|
|
119
|
+
/** @internal */
|
|
120
|
+
_attach(e, t) {
|
|
121
|
+
this.media = e, this.mic = t;
|
|
122
|
+
}
|
|
123
|
+
/** @internal */
|
|
124
|
+
_markAccepted() {
|
|
125
|
+
this.accepted || (this.accepted = !0, this.cbAccept.forEach((e) => e(this)), this.cbState.forEach((e) => e("accepted")));
|
|
126
|
+
}
|
|
127
|
+
/** @internal */
|
|
128
|
+
_markEnded(e) {
|
|
129
|
+
this.ended || (this.ended = !0, this.accepted || this.cbUnanswered.forEach((t) => t()), this.media?.close(), this.mic?.getTracks().forEach((t) => t.stop()), this.cbEnd.forEach((t) => t({ reason: e })), this.cbState.forEach((t) => t("ended")));
|
|
130
|
+
}
|
|
131
|
+
mute() {
|
|
132
|
+
this.mic?.getAudioTracks().forEach((e) => e.enabled = !1);
|
|
133
|
+
}
|
|
134
|
+
unmute() {
|
|
135
|
+
this.mic?.getAudioTracks().forEach((e) => e.enabled = !0);
|
|
136
|
+
}
|
|
137
|
+
async end() {
|
|
138
|
+
if (!this.ended) {
|
|
139
|
+
try {
|
|
140
|
+
await this.client.del(`/api/sessions/${this.sessionId}/calls/${this.callId}`);
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
this._markEnded("user_ended");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
class k {
|
|
148
|
+
constructor(e, t, s, i) {
|
|
149
|
+
this.dev = e, this.sessionId = t, this.callId = s, this.peer = i;
|
|
150
|
+
}
|
|
151
|
+
accept() {
|
|
152
|
+
return this.dev._answer(this.callId, this.peer);
|
|
153
|
+
}
|
|
154
|
+
async reject() {
|
|
155
|
+
await this.dev.client.post(`/api/sessions/${this.sessionId}/calls/${this.callId}/reject`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
class E {
|
|
159
|
+
constructor(e, t) {
|
|
160
|
+
this.client = e, this.parent = t, this.sessionId = "", this.name = "", this.connected = !1, this.es = null, this.calls = /* @__PURE__ */ new Map();
|
|
161
|
+
}
|
|
162
|
+
async start() {
|
|
163
|
+
const { sessions: e } = await this.client.get("/api/sessions"), t = e?.[0];
|
|
164
|
+
this.sessionId = t?.id ?? "", this.name = t?.name ?? "", this.connected = t?.state === "open", this.es = this.client.events((s) => this.onEvent(s)), this.parent._emitUpdate();
|
|
165
|
+
}
|
|
166
|
+
stop() {
|
|
167
|
+
this.es?.close(), this.es = null, this.calls.forEach((e) => e._markEnded("closed")), this.calls.clear();
|
|
168
|
+
}
|
|
169
|
+
onEvent(e) {
|
|
170
|
+
switch (e.type) {
|
|
171
|
+
case "incoming": {
|
|
172
|
+
if (!e.id) return;
|
|
173
|
+
const t = new k(this, e.sessionId || this.sessionId, e.id, { phone: b(e.peer) });
|
|
174
|
+
this.parent._emitOffer(t);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case "call-status": {
|
|
178
|
+
const t = e.id ? this.calls.get(e.id) : void 0;
|
|
179
|
+
t && e.status === "connected" && t._markAccepted();
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case "call-ended": {
|
|
183
|
+
if (!e.id) return;
|
|
184
|
+
const t = this.calls.get(e.id);
|
|
185
|
+
t && (t._markEnded(e.reason), this.calls.delete(e.id));
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case "auth-state": {
|
|
189
|
+
e.sessionId === this.sessionId && (this.connected = e.status === "open" || e.state === "open", this.parent._emitUpdate());
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case "call-peer": {
|
|
193
|
+
if (!e.id) return;
|
|
194
|
+
const t = e;
|
|
195
|
+
this.parent._emitPeerInfo({ callId: e.id, phone: t.peerPhone, name: t.peerName, avatar: t.peerAvatar });
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** @internal — accept an incoming call and bring up media. */
|
|
201
|
+
async _answer(e, t) {
|
|
202
|
+
await this.client.post(`/api/sessions/${this.sessionId}/calls/${e}/accept`);
|
|
203
|
+
const s = await m(this.parent.micDeviceId), i = await f(this.client, this.sessionId, e, s, this.parent.outputDeviceId, () => {
|
|
204
|
+
this.calls.get(e)?._markEnded("disconnected");
|
|
205
|
+
}), r = new w(this.client, this.sessionId, e, t, "inbound");
|
|
206
|
+
return r._attach(i, s), r._markAccepted(), this.calls.set(e, r), r;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
class _ {
|
|
210
|
+
constructor(e) {
|
|
211
|
+
this.config = e, this.devices = /* @__PURE__ */ new Map(), this.offerCbs = [], this.updateCbs = [], this.peerInfoCbs = [], this.micDeviceId = e.micDeviceId, this.outputDeviceId = e.outputDeviceId, e.tokens?.length && this.addDevices(e.tokens);
|
|
212
|
+
}
|
|
213
|
+
/** onUpdate fires when the device list or any device's connection changes. */
|
|
214
|
+
onUpdate(e) {
|
|
215
|
+
return this.updateCbs.push(e), this;
|
|
216
|
+
}
|
|
217
|
+
/** @internal */
|
|
218
|
+
_emitUpdate() {
|
|
219
|
+
this.updateCbs.forEach((e) => e());
|
|
220
|
+
}
|
|
221
|
+
/** Devices currently registered in this instance (for a "Números" UI). */
|
|
222
|
+
getDevices() {
|
|
223
|
+
return [...this.devices.entries()].map(([e, t]) => ({
|
|
224
|
+
token: e,
|
|
225
|
+
name: this.config.deviceNames?.[e] || t.name || e.slice(0, 10),
|
|
226
|
+
connected: t.connected
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
async addDevices(e) {
|
|
230
|
+
for (const t of e) {
|
|
231
|
+
if (this.devices.has(t)) continue;
|
|
232
|
+
const s = new E(new g(this.config.baseUrl, t), this);
|
|
233
|
+
this.devices.set(t, s);
|
|
234
|
+
try {
|
|
235
|
+
await s.start();
|
|
236
|
+
} catch {
|
|
237
|
+
this.devices.delete(t);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
this._emitUpdate();
|
|
241
|
+
}
|
|
242
|
+
removeDevices(e) {
|
|
243
|
+
for (const t of e)
|
|
244
|
+
this.devices.get(t)?.stop(), this.devices.delete(t);
|
|
245
|
+
this._emitUpdate();
|
|
246
|
+
}
|
|
247
|
+
onOffer(e) {
|
|
248
|
+
return this.offerCbs.push(e), this;
|
|
249
|
+
}
|
|
250
|
+
/** onPeerInfo fires when the backend resolves a call's contact (phone/name/avatar). */
|
|
251
|
+
onPeerInfo(e) {
|
|
252
|
+
return this.peerInfoCbs.push(e), this;
|
|
253
|
+
}
|
|
254
|
+
/** @internal */
|
|
255
|
+
_emitOffer(e) {
|
|
256
|
+
this.offerCbs.forEach((t) => t(e));
|
|
257
|
+
}
|
|
258
|
+
/** @internal */
|
|
259
|
+
_emitPeerInfo(e) {
|
|
260
|
+
this.peerInfoCbs.forEach((t) => t(e));
|
|
261
|
+
}
|
|
262
|
+
async startCall({ to: e, fromTokens: t }) {
|
|
263
|
+
const s = t?.[0] ?? [...this.devices.keys()][0], i = s ? this.devices.get(s) : void 0;
|
|
264
|
+
if (!i || !i.sessionId) return { err: "no device available" };
|
|
265
|
+
try {
|
|
266
|
+
const { call: r } = await i.client.post(
|
|
267
|
+
`/api/sessions/${i.sessionId}/calls`,
|
|
268
|
+
{ phone: e }
|
|
269
|
+
), a = r.callId, d = await m(this.micDeviceId), o = await f(i.client, i.sessionId, a, d, this.outputDeviceId, () => {
|
|
270
|
+
i.calls.get(a)?._markEnded("disconnected");
|
|
271
|
+
}), n = new w(i.client, i.sessionId, a, { phone: e }, "outbound");
|
|
272
|
+
return n._attach(o, d), i.calls.set(a, n), { call: n };
|
|
273
|
+
} catch (r) {
|
|
274
|
+
return { err: r.message };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async getMultimediaDevices() {
|
|
278
|
+
const e = await navigator.mediaDevices.enumerateDevices();
|
|
279
|
+
return {
|
|
280
|
+
microphones: e.filter((t) => t.kind === "audioinput"),
|
|
281
|
+
speakers: e.filter((t) => t.kind === "audiooutput")
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
setMic(e) {
|
|
285
|
+
this.micDeviceId = e;
|
|
286
|
+
}
|
|
287
|
+
setOutput(e) {
|
|
288
|
+
this.outputDeviceId = e;
|
|
289
|
+
}
|
|
290
|
+
close() {
|
|
291
|
+
this.devices.forEach((e) => e.stop()), this.devices.clear();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
export {
|
|
295
|
+
w as Call,
|
|
296
|
+
_ as DigiCalls,
|
|
297
|
+
k as Offer,
|
|
298
|
+
_ as default
|
|
299
|
+
};
|
|
300
|
+
//# sourceMappingURL=digicalls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digicalls.js","sources":["../src/client.ts","../src/webrtc.ts","../src/index.ts"],"sourcesContent":["// HTTP + SSE client for one device token against a DigiCalls backend.\n\nconst FALLBACK_ICE: RTCIceServer[] = [{ urls: \"stun:stun.l.google.com:19302\" }];\n\nexport type BrokerEvent = {\n type: string;\n sessionId?: string;\n id?: string;\n peer?: string;\n status?: string;\n reason?: string;\n [k: string]: unknown;\n};\n\nexport class Client {\n readonly clientId: string;\n private iceCache: RTCIceServer[] | null = null;\n\n constructor(\n readonly baseUrl: string,\n readonly token: string,\n clientId?: string,\n ) {\n this.baseUrl = baseUrl.replace(/\\/+$/, \"\");\n this.clientId = clientId ?? \"dc_\" + Math.random().toString(36).slice(2, 12);\n }\n\n private headers(): Record<string, string> {\n return {\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer \" + this.token,\n \"X-Client-Id\": this.clientId,\n };\n }\n\n async get<T>(path: string): Promise<T> {\n const r = await fetch(this.baseUrl + path, { headers: this.headers() });\n if (!r.ok) throw new Error(`${path} ${r.status}`);\n return (await r.json()) as T;\n }\n\n async post<T>(path: string, body: unknown = {}): Promise<T> {\n const r = await fetch(this.baseUrl + path, {\n method: \"POST\",\n headers: this.headers(),\n body: JSON.stringify(body),\n });\n if (!r.ok) throw new Error(`${path} ${r.status}`);\n if (r.status === 204) return undefined as T;\n return (await r.json()) as T;\n }\n\n async del(path: string): Promise<void> {\n const r = await fetch(this.baseUrl + path, { method: \"DELETE\", headers: this.headers() });\n if (!r.ok && r.status !== 204) throw new Error(`${path} ${r.status}`);\n }\n\n // events opens the SSE stream for this device token. EventSource can't send\n // headers, so the token goes in the query (the backend reads ?token=).\n events(onEvent: (ev: BrokerEvent) => void): EventSource {\n const u = new URL(this.baseUrl + \"/api/events\");\n u.searchParams.set(\"token\", this.token);\n u.searchParams.set(\"clientId\", this.clientId);\n const es = new EventSource(u.toString());\n es.onmessage = (e) => {\n try {\n onEvent(JSON.parse(e.data));\n } catch {\n /* ignore keepalives / malformed */\n }\n };\n return es;\n }\n\n async iceServers(): Promise<RTCIceServer[]> {\n if (this.iceCache) return this.iceCache;\n try {\n const { iceServers } = await this.get<{ iceServers: RTCIceServer[] }>(\"/api/webrtc-config\");\n this.iceCache = iceServers?.length ? iceServers : FALLBACK_ICE;\n } catch {\n this.iceCache = FALLBACK_ICE;\n }\n return this.iceCache;\n }\n}\n","import type { Client } from \"./client\";\n\nconst waitForIce = (pc: RTCPeerConnection, timeoutMs = 2000): Promise<void> =>\n new Promise((resolve) => {\n if (pc.iceGatheringState === \"complete\") return resolve();\n const done = () => {\n clearTimeout(timer);\n pc.removeEventListener(\"icegatheringstatechange\", onChange);\n resolve();\n };\n const onChange = () => pc.iceGatheringState === \"complete\" && done();\n const timer = setTimeout(done, timeoutMs);\n pc.addEventListener(\"icegatheringstatechange\", onChange);\n });\n\nexport type Media = {\n pc: RTCPeerConnection;\n remote: MediaStream;\n close: () => void;\n};\n\n// negotiate sets up the RTCPeerConnection, exchanges SDP with the DigiCalls\n// backend (POST /calls/{id}/webrtc), and auto-plays the remote audio.\nexport async function negotiate(\n client: Client,\n sessionId: string,\n callId: string,\n micStream: MediaStream | null,\n outputDeviceId: string | undefined,\n onClose: () => void,\n): Promise<Media> {\n const pc = new RTCPeerConnection({ iceServers: await client.iceServers() });\n const sendTx = pc.addTransceiver(\"audio\", { direction: \"sendonly\" });\n if (micStream) {\n const track = micStream.getAudioTracks()[0];\n if (track) await sendTx.sender.replaceTrack(track);\n }\n pc.addTransceiver(\"audio\", { direction: \"recvonly\" });\n\n const remote = new MediaStream();\n const audioEl = typeof document !== \"undefined\" ? document.createElement(\"audio\") : null;\n if (audioEl) {\n audioEl.autoplay = true;\n (audioEl as HTMLAudioElement & { playsInline?: boolean }).playsInline = true;\n audioEl.srcObject = remote;\n }\n pc.ontrack = (e) => {\n const stream = e.streams[0];\n if (stream) stream.getTracks().forEach((t) => remote.addTrack(t));\n else remote.addTrack(e.track);\n if (audioEl) {\n audioEl.srcObject = remote;\n const sink = audioEl as HTMLAudioElement & { setSinkId?: (id: string) => Promise<void> };\n if (outputDeviceId && sink.setSinkId) sink.setSinkId(outputDeviceId).catch(() => {});\n audioEl.play().catch(() => {});\n }\n };\n pc.onconnectionstatechange = () => {\n if ([\"failed\", \"closed\", \"disconnected\"].includes(pc.connectionState)) onClose();\n };\n\n await pc.setLocalDescription(await pc.createOffer());\n await waitForIce(pc);\n const { sdp_answer } = await client.post<{ sdp_answer: string }>(\n `/api/sessions/${sessionId}/calls/${callId}/webrtc`,\n { sdp_offer: pc.localDescription!.sdp },\n );\n await pc.setRemoteDescription({ type: \"answer\", sdp: sdp_answer });\n\n const close = () => {\n try {\n pc.getSenders().forEach((s) => s.track?.stop());\n pc.close();\n } catch {\n /* noop */\n }\n if (audioEl) {\n audioEl.srcObject = null;\n audioEl.remove();\n }\n };\n return { pc, remote, close };\n}\n","import { Client, type BrokerEvent } from \"./client\";\nimport { negotiate, type Media } from \"./webrtc\";\n\nexport interface Peer {\n phone: string;\n name?: string;\n avatar?: string;\n}\n\nexport interface PeerInfo {\n callId: string;\n phone?: string;\n name?: string;\n avatar?: string;\n}\n\nexport interface DigiCallsConfig {\n /** DigiCalls backend, e.g. https://calls.digitalsac.io */\n baseUrl: string;\n /** Device tokens (one per WhatsApp connection). */\n tokens?: string[];\n /** token -> friendly line name. */\n deviceNames?: Record<string, string>;\n micDeviceId?: string;\n outputDeviceId?: string;\n}\n\ntype CB<T> = (arg: T) => void;\n\nfunction phoneOf(jidOrPhone?: string): string {\n if (!jidOrPhone) return \"\";\n return jidOrPhone.split(\"@\")[0].replace(/[^0-9]/g, \"\");\n}\n\nasync function getMic(deviceId?: string): Promise<MediaStream> {\n return navigator.mediaDevices.getUserMedia({\n audio: deviceId ? { deviceId: { exact: deviceId } } : true,\n });\n}\n\n/** A call (outgoing or accepted). */\nexport class Call {\n private media: Media | null = null;\n private mic: MediaStream | null = null;\n private accepted = false;\n private ended = false;\n private cbAccept: CB<Call>[] = [];\n private cbEnd: CB<{ reason?: string }>[] = [];\n private cbUnanswered: CB<void>[] = [];\n private cbState: CB<string>[] = [];\n\n constructor(\n private readonly client: Client,\n readonly sessionId: string,\n readonly callId: string,\n readonly peer: Peer,\n readonly direction: \"outbound\" | \"inbound\",\n ) {}\n\n onPeerAccept(cb: CB<Call>): this {\n this.cbAccept.push(cb);\n if (this.accepted) cb(this);\n return this;\n }\n onEnd(cb: CB<{ reason?: string }>): this {\n this.cbEnd.push(cb);\n return this;\n }\n onUnanswered(cb: CB<void>): this {\n this.cbUnanswered.push(cb);\n return this;\n }\n onStateChange(cb: CB<string>): this {\n this.cbState.push(cb);\n return this;\n }\n\n /** @internal */\n _attach(media: Media, mic: MediaStream | null): void {\n this.media = media;\n this.mic = mic;\n }\n /** @internal */\n _markAccepted(): void {\n if (this.accepted) return;\n this.accepted = true;\n this.cbAccept.forEach((c) => c(this));\n this.cbState.forEach((c) => c(\"accepted\"));\n }\n /** @internal */\n _markEnded(reason?: string): void {\n if (this.ended) return;\n this.ended = true;\n if (!this.accepted) this.cbUnanswered.forEach((c) => c());\n this.media?.close();\n this.mic?.getTracks().forEach((t) => t.stop());\n this.cbEnd.forEach((c) => c({ reason }));\n this.cbState.forEach((c) => c(\"ended\"));\n }\n\n mute(): void {\n this.mic?.getAudioTracks().forEach((t) => (t.enabled = false));\n }\n unmute(): void {\n this.mic?.getAudioTracks().forEach((t) => (t.enabled = true));\n }\n async end(): Promise<void> {\n if (this.ended) return;\n try {\n await this.client.del(`/api/sessions/${this.sessionId}/calls/${this.callId}`);\n } catch {\n /* server may already have ended it */\n }\n this._markEnded(\"user_ended\");\n }\n}\n\n/** An incoming call offer. */\nexport class Offer {\n constructor(\n private readonly dev: Device,\n readonly sessionId: string,\n readonly callId: string,\n readonly peer: Peer,\n ) {}\n\n accept(): Promise<Call> {\n return this.dev._answer(this.callId, this.peer);\n }\n async reject(): Promise<void> {\n await this.dev.client.post(`/api/sessions/${this.sessionId}/calls/${this.callId}/reject`);\n }\n}\n\n/** Internal per-token device: resolves its session, holds the SSE stream. */\nclass Device {\n sessionId = \"\";\n name = \"\";\n connected = false;\n es: EventSource | null = null;\n calls = new Map<string, Call>();\n\n constructor(\n readonly client: Client,\n private readonly parent: DigiCalls,\n ) {}\n\n async start(): Promise<void> {\n const { sessions } = await this.client.get<{\n sessions: { id: string; name?: string; state?: string; paired?: boolean }[];\n }>(\"/api/sessions\");\n const s = sessions?.[0];\n this.sessionId = s?.id ?? \"\";\n this.name = s?.name ?? \"\";\n this.connected = s?.state === \"open\";\n this.es = this.client.events((ev) => this.onEvent(ev));\n this.parent._emitUpdate();\n }\n\n stop(): void {\n this.es?.close();\n this.es = null;\n this.calls.forEach((c) => c._markEnded(\"closed\"));\n this.calls.clear();\n }\n\n private onEvent(ev: BrokerEvent): void {\n switch (ev.type) {\n case \"incoming\": {\n if (!ev.id) return;\n const offer = new Offer(this, ev.sessionId || this.sessionId, ev.id, { phone: phoneOf(ev.peer) });\n this.parent._emitOffer(offer);\n break;\n }\n case \"call-status\": {\n const c = ev.id ? this.calls.get(ev.id) : undefined;\n if (c && ev.status === \"connected\") c._markAccepted();\n break;\n }\n case \"call-ended\": {\n if (!ev.id) return;\n const c = this.calls.get(ev.id);\n if (c) {\n c._markEnded(ev.reason);\n this.calls.delete(ev.id);\n }\n break;\n }\n case \"auth-state\": {\n if (ev.sessionId === this.sessionId) {\n this.connected = ev.status === \"open\" || (ev as { state?: string }).state === \"open\";\n this.parent._emitUpdate();\n }\n break;\n }\n case \"call-peer\": {\n if (!ev.id) return;\n const e = ev as { peerPhone?: string; peerName?: string; peerAvatar?: string };\n this.parent._emitPeerInfo({ callId: ev.id, phone: e.peerPhone, name: e.peerName, avatar: e.peerAvatar });\n break;\n }\n }\n }\n\n /** @internal — accept an incoming call and bring up media. */\n async _answer(callId: string, peer: Peer): Promise<Call> {\n await this.client.post(`/api/sessions/${this.sessionId}/calls/${callId}/accept`);\n const mic = await getMic(this.parent.micDeviceId);\n const media = await negotiate(this.client, this.sessionId, callId, mic, this.parent.outputDeviceId, () => {\n this.calls.get(callId)?._markEnded(\"disconnected\");\n });\n const call = new Call(this.client, this.sessionId, callId, peer, \"inbound\");\n call._attach(media, mic);\n call._markAccepted();\n this.calls.set(callId, call);\n return call;\n }\n}\n\n/**\n * DigiCalls — WhatsApp calls SDK:\n * const dc = new DigiCalls({ baseUrl, tokens });\n * dc.onOffer(o => o.accept());\n * const { call, err } = await dc.startCall({ fromTokens, to });\n */\nexport class DigiCalls {\n micDeviceId?: string;\n outputDeviceId?: string;\n private devices = new Map<string, Device>();\n private offerCbs: CB<Offer>[] = [];\n private updateCbs: CB<void>[] = [];\n private peerInfoCbs: CB<PeerInfo>[] = [];\n\n constructor(private readonly config: DigiCallsConfig) {\n this.micDeviceId = config.micDeviceId;\n this.outputDeviceId = config.outputDeviceId;\n if (config.tokens?.length) void this.addDevices(config.tokens);\n }\n\n /** onUpdate fires when the device list or any device's connection changes. */\n onUpdate(cb: CB<void>): this {\n this.updateCbs.push(cb);\n return this;\n }\n /** @internal */\n _emitUpdate(): void {\n this.updateCbs.forEach((c) => c());\n }\n\n /** Devices currently registered in this instance (for a \"Números\" UI). */\n getDevices(): { token: string; name: string; connected: boolean }[] {\n return [...this.devices.entries()].map(([token, d]) => ({\n token,\n name: this.config.deviceNames?.[token] || d.name || token.slice(0, 10),\n connected: d.connected,\n }));\n }\n\n async addDevices(tokens: string[]): Promise<void> {\n for (const t of tokens) {\n if (this.devices.has(t)) continue;\n const dev = new Device(new Client(this.config.baseUrl, t), this);\n this.devices.set(t, dev);\n try {\n await dev.start();\n } catch {\n this.devices.delete(t);\n }\n }\n this._emitUpdate();\n }\n\n removeDevices(tokens: string[]): void {\n for (const t of tokens) {\n this.devices.get(t)?.stop();\n this.devices.delete(t);\n }\n this._emitUpdate();\n }\n\n onOffer(cb: CB<Offer>): this {\n this.offerCbs.push(cb);\n return this;\n }\n\n /** onPeerInfo fires when the backend resolves a call's contact (phone/name/avatar). */\n onPeerInfo(cb: CB<PeerInfo>): this {\n this.peerInfoCbs.push(cb);\n return this;\n }\n\n /** @internal */\n _emitOffer(o: Offer): void {\n this.offerCbs.forEach((c) => c(o));\n }\n /** @internal */\n _emitPeerInfo(info: PeerInfo): void {\n this.peerInfoCbs.forEach((c) => c(info));\n }\n\n async startCall({ to, fromTokens }: { to: string; fromTokens?: string[] }): Promise<{ call?: Call; err?: string }> {\n const token = fromTokens?.[0] ?? [...this.devices.keys()][0];\n const dev = token ? this.devices.get(token) : undefined;\n if (!dev || !dev.sessionId) return { err: \"no device available\" };\n try {\n const { call: created } = await dev.client.post<{ call: { callId: string } }>(\n `/api/sessions/${dev.sessionId}/calls`,\n { phone: to },\n );\n const callId = created.callId;\n const mic = await getMic(this.micDeviceId);\n const media = await negotiate(dev.client, dev.sessionId, callId, mic, this.outputDeviceId, () => {\n dev.calls.get(callId)?._markEnded(\"disconnected\");\n });\n const call = new Call(dev.client, dev.sessionId, callId, { phone: to }, \"outbound\");\n call._attach(media, mic);\n dev.calls.set(callId, call);\n return { call };\n } catch (e) {\n return { err: (e as Error).message };\n }\n }\n\n async getMultimediaDevices(): Promise<{ microphones: MediaDeviceInfo[]; speakers: MediaDeviceInfo[] }> {\n const list = await navigator.mediaDevices.enumerateDevices();\n return {\n microphones: list.filter((d) => d.kind === \"audioinput\"),\n speakers: list.filter((d) => d.kind === \"audiooutput\"),\n };\n }\n\n setMic(deviceId?: string): void {\n this.micDeviceId = deviceId;\n }\n setOutput(deviceId?: string): void {\n this.outputDeviceId = deviceId;\n }\n\n close(): void {\n this.devices.forEach((d) => d.stop());\n this.devices.clear();\n }\n}\n\nexport default DigiCalls;\n"],"names":["FALLBACK_ICE","Client","baseUrl","token","clientId","path","r","body","onEvent","u","es","e","iceServers","waitForIce","pc","timeoutMs","resolve","done","timer","onChange","negotiate","client","sessionId","callId","micStream","outputDeviceId","onClose","sendTx","track","remote","audioEl","stream","t","sink","sdp_answer","s","phoneOf","jidOrPhone","getMic","deviceId","Call","peer","direction","cb","media","mic","c","reason","Offer","dev","Device","parent","sessions","ev","offer","call","DigiCalls","config","d","tokens","o","info","to","fromTokens","created","list"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
(function(o,l){typeof exports=="object"&&typeof module<"u"?l(exports):typeof define=="function"&&define.amd?define(["exports"],l):(o=typeof globalThis<"u"?globalThis:o||self,l(o.DigiCalls={}))})(this,(function(o){"use strict";const l=[{urls:"stun:stun.l.google.com:19302"}];class I{constructor(e,t,s){this.baseUrl=e,this.token=t,this.iceCache=null,this.baseUrl=e.replace(/\/+$/,""),this.clientId=s??"dc_"+Math.random().toString(36).slice(2,12)}headers(){return{"Content-Type":"application/json",Authorization:"Bearer "+this.token,"X-Client-Id":this.clientId}}async get(e){const t=await fetch(this.baseUrl+e,{headers:this.headers()});if(!t.ok)throw new Error(`${e} ${t.status}`);return await t.json()}async post(e,t={}){const s=await fetch(this.baseUrl+e,{method:"POST",headers:this.headers(),body:JSON.stringify(t)});if(!s.ok)throw new Error(`${e} ${s.status}`);if(s.status!==204)return await s.json()}async del(e){const t=await fetch(this.baseUrl+e,{method:"DELETE",headers:this.headers()});if(!t.ok&&t.status!==204)throw new Error(`${e} ${t.status}`)}events(e){const t=new URL(this.baseUrl+"/api/events");t.searchParams.set("token",this.token),t.searchParams.set("clientId",this.clientId);const s=new EventSource(t.toString());return s.onmessage=i=>{try{e(JSON.parse(i.data))}catch{}},s}async iceServers(){if(this.iceCache)return this.iceCache;try{const{iceServers:e}=await this.get("/api/webrtc-config");this.iceCache=e?.length?e:l}catch{this.iceCache=l}return this.iceCache}}const k=(c,e=2e3)=>new Promise(t=>{if(c.iceGatheringState==="complete")return t();const s=()=>{clearTimeout(r),c.removeEventListener("icegatheringstatechange",i),t()},i=()=>c.iceGatheringState==="complete"&&s(),r=setTimeout(s,e);c.addEventListener("icegatheringstatechange",i)});async function m(c,e,t,s,i,r){const n=new RTCPeerConnection({iceServers:await c.iceServers()}),u=n.addTransceiver("audio",{direction:"sendonly"});if(s){const d=s.getAudioTracks()[0];d&&await u.sender.replaceTrack(d)}n.addTransceiver("audio",{direction:"recvonly"});const h=new MediaStream,a=typeof document<"u"?document.createElement("audio"):null;a&&(a.autoplay=!0,a.playsInline=!0,a.srcObject=h),n.ontrack=d=>{const b=d.streams[0];if(b?b.getTracks().forEach(p=>h.addTrack(p)):h.addTrack(d.track),a){a.srcObject=h;const p=a;i&&p.setSinkId&&p.setSinkId(i).catch(()=>{}),a.play().catch(()=>{})}},n.onconnectionstatechange=()=>{["failed","closed","disconnected"].includes(n.connectionState)&&r()},await n.setLocalDescription(await n.createOffer()),await k(n);const{sdp_answer:C}=await c.post(`/api/sessions/${e}/calls/${t}/webrtc`,{sdp_offer:n.localDescription.sdp});return await n.setRemoteDescription({type:"answer",sdp:C}),{pc:n,remote:h,close:()=>{try{n.getSenders().forEach(d=>d.track?.stop()),n.close()}catch{}a&&(a.srcObject=null,a.remove())}}}function E(c){return c?c.split("@")[0].replace(/[^0-9]/g,""):""}async function w(c){return navigator.mediaDevices.getUserMedia({audio:c?{deviceId:{exact:c}}:!0})}class f{constructor(e,t,s,i,r){this.client=e,this.sessionId=t,this.callId=s,this.peer=i,this.direction=r,this.media=null,this.mic=null,this.accepted=!1,this.ended=!1,this.cbAccept=[],this.cbEnd=[],this.cbUnanswered=[],this.cbState=[]}onPeerAccept(e){return this.cbAccept.push(e),this.accepted&&e(this),this}onEnd(e){return this.cbEnd.push(e),this}onUnanswered(e){return this.cbUnanswered.push(e),this}onStateChange(e){return this.cbState.push(e),this}_attach(e,t){this.media=e,this.mic=t}_markAccepted(){this.accepted||(this.accepted=!0,this.cbAccept.forEach(e=>e(this)),this.cbState.forEach(e=>e("accepted")))}_markEnded(e){this.ended||(this.ended=!0,this.accepted||this.cbUnanswered.forEach(t=>t()),this.media?.close(),this.mic?.getTracks().forEach(t=>t.stop()),this.cbEnd.forEach(t=>t({reason:e})),this.cbState.forEach(t=>t("ended")))}mute(){this.mic?.getAudioTracks().forEach(e=>e.enabled=!1)}unmute(){this.mic?.getAudioTracks().forEach(e=>e.enabled=!0)}async end(){if(!this.ended){try{await this.client.del(`/api/sessions/${this.sessionId}/calls/${this.callId}`)}catch{}this._markEnded("user_ended")}}}class g{constructor(e,t,s,i){this.dev=e,this.sessionId=t,this.callId=s,this.peer=i}accept(){return this.dev._answer(this.callId,this.peer)}async reject(){await this.dev.client.post(`/api/sessions/${this.sessionId}/calls/${this.callId}/reject`)}}class y{constructor(e,t){this.client=e,this.parent=t,this.sessionId="",this.name="",this.connected=!1,this.es=null,this.calls=new Map}async start(){const{sessions:e}=await this.client.get("/api/sessions"),t=e?.[0];this.sessionId=t?.id??"",this.name=t?.name??"",this.connected=t?.state==="open",this.es=this.client.events(s=>this.onEvent(s)),this.parent._emitUpdate()}stop(){this.es?.close(),this.es=null,this.calls.forEach(e=>e._markEnded("closed")),this.calls.clear()}onEvent(e){switch(e.type){case"incoming":{if(!e.id)return;const t=new g(this,e.sessionId||this.sessionId,e.id,{phone:E(e.peer)});this.parent._emitOffer(t);break}case"call-status":{const t=e.id?this.calls.get(e.id):void 0;t&&e.status==="connected"&&t._markAccepted();break}case"call-ended":{if(!e.id)return;const t=this.calls.get(e.id);t&&(t._markEnded(e.reason),this.calls.delete(e.id));break}case"auth-state":{e.sessionId===this.sessionId&&(this.connected=e.status==="open"||e.state==="open",this.parent._emitUpdate());break}case"call-peer":{if(!e.id)return;const t=e;this.parent._emitPeerInfo({callId:e.id,phone:t.peerPhone,name:t.peerName,avatar:t.peerAvatar});break}}}async _answer(e,t){await this.client.post(`/api/sessions/${this.sessionId}/calls/${e}/accept`);const s=await w(this.parent.micDeviceId),i=await m(this.client,this.sessionId,e,s,this.parent.outputDeviceId,()=>{this.calls.get(e)?._markEnded("disconnected")}),r=new f(this.client,this.sessionId,e,t,"inbound");return r._attach(i,s),r._markAccepted(),this.calls.set(e,r),r}}class v{constructor(e){this.config=e,this.devices=new Map,this.offerCbs=[],this.updateCbs=[],this.peerInfoCbs=[],this.micDeviceId=e.micDeviceId,this.outputDeviceId=e.outputDeviceId,e.tokens?.length&&this.addDevices(e.tokens)}onUpdate(e){return this.updateCbs.push(e),this}_emitUpdate(){this.updateCbs.forEach(e=>e())}getDevices(){return[...this.devices.entries()].map(([e,t])=>({token:e,name:this.config.deviceNames?.[e]||t.name||e.slice(0,10),connected:t.connected}))}async addDevices(e){for(const t of e){if(this.devices.has(t))continue;const s=new y(new I(this.config.baseUrl,t),this);this.devices.set(t,s);try{await s.start()}catch{this.devices.delete(t)}}this._emitUpdate()}removeDevices(e){for(const t of e)this.devices.get(t)?.stop(),this.devices.delete(t);this._emitUpdate()}onOffer(e){return this.offerCbs.push(e),this}onPeerInfo(e){return this.peerInfoCbs.push(e),this}_emitOffer(e){this.offerCbs.forEach(t=>t(e))}_emitPeerInfo(e){this.peerInfoCbs.forEach(t=>t(e))}async startCall({to:e,fromTokens:t}){const s=t?.[0]??[...this.devices.keys()][0],i=s?this.devices.get(s):void 0;if(!i||!i.sessionId)return{err:"no device available"};try{const{call:r}=await i.client.post(`/api/sessions/${i.sessionId}/calls`,{phone:e}),n=r.callId,u=await w(this.micDeviceId),h=await m(i.client,i.sessionId,n,u,this.outputDeviceId,()=>{i.calls.get(n)?._markEnded("disconnected")}),a=new f(i.client,i.sessionId,n,{phone:e},"outbound");return a._attach(h,u),i.calls.set(n,a),{call:a}}catch(r){return{err:r.message}}}async getMultimediaDevices(){const e=await navigator.mediaDevices.enumerateDevices();return{microphones:e.filter(t=>t.kind==="audioinput"),speakers:e.filter(t=>t.kind==="audiooutput")}}setMic(e){this.micDeviceId=e}setOutput(e){this.outputDeviceId=e}close(){this.devices.forEach(e=>e.stop()),this.devices.clear()}}o.Call=f,o.DigiCalls=v,o.Offer=g,o.default=v,Object.defineProperties(o,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
|
|
2
|
+
//# sourceMappingURL=digicalls.umd.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digicalls.umd.cjs","sources":["../src/client.ts","../src/webrtc.ts","../src/index.ts"],"sourcesContent":["// HTTP + SSE client for one device token against a DigiCalls backend.\n\nconst FALLBACK_ICE: RTCIceServer[] = [{ urls: \"stun:stun.l.google.com:19302\" }];\n\nexport type BrokerEvent = {\n type: string;\n sessionId?: string;\n id?: string;\n peer?: string;\n status?: string;\n reason?: string;\n [k: string]: unknown;\n};\n\nexport class Client {\n readonly clientId: string;\n private iceCache: RTCIceServer[] | null = null;\n\n constructor(\n readonly baseUrl: string,\n readonly token: string,\n clientId?: string,\n ) {\n this.baseUrl = baseUrl.replace(/\\/+$/, \"\");\n this.clientId = clientId ?? \"dc_\" + Math.random().toString(36).slice(2, 12);\n }\n\n private headers(): Record<string, string> {\n return {\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer \" + this.token,\n \"X-Client-Id\": this.clientId,\n };\n }\n\n async get<T>(path: string): Promise<T> {\n const r = await fetch(this.baseUrl + path, { headers: this.headers() });\n if (!r.ok) throw new Error(`${path} ${r.status}`);\n return (await r.json()) as T;\n }\n\n async post<T>(path: string, body: unknown = {}): Promise<T> {\n const r = await fetch(this.baseUrl + path, {\n method: \"POST\",\n headers: this.headers(),\n body: JSON.stringify(body),\n });\n if (!r.ok) throw new Error(`${path} ${r.status}`);\n if (r.status === 204) return undefined as T;\n return (await r.json()) as T;\n }\n\n async del(path: string): Promise<void> {\n const r = await fetch(this.baseUrl + path, { method: \"DELETE\", headers: this.headers() });\n if (!r.ok && r.status !== 204) throw new Error(`${path} ${r.status}`);\n }\n\n // events opens the SSE stream for this device token. EventSource can't send\n // headers, so the token goes in the query (the backend reads ?token=).\n events(onEvent: (ev: BrokerEvent) => void): EventSource {\n const u = new URL(this.baseUrl + \"/api/events\");\n u.searchParams.set(\"token\", this.token);\n u.searchParams.set(\"clientId\", this.clientId);\n const es = new EventSource(u.toString());\n es.onmessage = (e) => {\n try {\n onEvent(JSON.parse(e.data));\n } catch {\n /* ignore keepalives / malformed */\n }\n };\n return es;\n }\n\n async iceServers(): Promise<RTCIceServer[]> {\n if (this.iceCache) return this.iceCache;\n try {\n const { iceServers } = await this.get<{ iceServers: RTCIceServer[] }>(\"/api/webrtc-config\");\n this.iceCache = iceServers?.length ? iceServers : FALLBACK_ICE;\n } catch {\n this.iceCache = FALLBACK_ICE;\n }\n return this.iceCache;\n }\n}\n","import type { Client } from \"./client\";\n\nconst waitForIce = (pc: RTCPeerConnection, timeoutMs = 2000): Promise<void> =>\n new Promise((resolve) => {\n if (pc.iceGatheringState === \"complete\") return resolve();\n const done = () => {\n clearTimeout(timer);\n pc.removeEventListener(\"icegatheringstatechange\", onChange);\n resolve();\n };\n const onChange = () => pc.iceGatheringState === \"complete\" && done();\n const timer = setTimeout(done, timeoutMs);\n pc.addEventListener(\"icegatheringstatechange\", onChange);\n });\n\nexport type Media = {\n pc: RTCPeerConnection;\n remote: MediaStream;\n close: () => void;\n};\n\n// negotiate sets up the RTCPeerConnection, exchanges SDP with the DigiCalls\n// backend (POST /calls/{id}/webrtc), and auto-plays the remote audio.\nexport async function negotiate(\n client: Client,\n sessionId: string,\n callId: string,\n micStream: MediaStream | null,\n outputDeviceId: string | undefined,\n onClose: () => void,\n): Promise<Media> {\n const pc = new RTCPeerConnection({ iceServers: await client.iceServers() });\n const sendTx = pc.addTransceiver(\"audio\", { direction: \"sendonly\" });\n if (micStream) {\n const track = micStream.getAudioTracks()[0];\n if (track) await sendTx.sender.replaceTrack(track);\n }\n pc.addTransceiver(\"audio\", { direction: \"recvonly\" });\n\n const remote = new MediaStream();\n const audioEl = typeof document !== \"undefined\" ? document.createElement(\"audio\") : null;\n if (audioEl) {\n audioEl.autoplay = true;\n (audioEl as HTMLAudioElement & { playsInline?: boolean }).playsInline = true;\n audioEl.srcObject = remote;\n }\n pc.ontrack = (e) => {\n const stream = e.streams[0];\n if (stream) stream.getTracks().forEach((t) => remote.addTrack(t));\n else remote.addTrack(e.track);\n if (audioEl) {\n audioEl.srcObject = remote;\n const sink = audioEl as HTMLAudioElement & { setSinkId?: (id: string) => Promise<void> };\n if (outputDeviceId && sink.setSinkId) sink.setSinkId(outputDeviceId).catch(() => {});\n audioEl.play().catch(() => {});\n }\n };\n pc.onconnectionstatechange = () => {\n if ([\"failed\", \"closed\", \"disconnected\"].includes(pc.connectionState)) onClose();\n };\n\n await pc.setLocalDescription(await pc.createOffer());\n await waitForIce(pc);\n const { sdp_answer } = await client.post<{ sdp_answer: string }>(\n `/api/sessions/${sessionId}/calls/${callId}/webrtc`,\n { sdp_offer: pc.localDescription!.sdp },\n );\n await pc.setRemoteDescription({ type: \"answer\", sdp: sdp_answer });\n\n const close = () => {\n try {\n pc.getSenders().forEach((s) => s.track?.stop());\n pc.close();\n } catch {\n /* noop */\n }\n if (audioEl) {\n audioEl.srcObject = null;\n audioEl.remove();\n }\n };\n return { pc, remote, close };\n}\n","import { Client, type BrokerEvent } from \"./client\";\nimport { negotiate, type Media } from \"./webrtc\";\n\nexport interface Peer {\n phone: string;\n name?: string;\n avatar?: string;\n}\n\nexport interface PeerInfo {\n callId: string;\n phone?: string;\n name?: string;\n avatar?: string;\n}\n\nexport interface DigiCallsConfig {\n /** DigiCalls backend, e.g. https://calls.digitalsac.io */\n baseUrl: string;\n /** Device tokens (one per WhatsApp connection). */\n tokens?: string[];\n /** token -> friendly line name. */\n deviceNames?: Record<string, string>;\n micDeviceId?: string;\n outputDeviceId?: string;\n}\n\ntype CB<T> = (arg: T) => void;\n\nfunction phoneOf(jidOrPhone?: string): string {\n if (!jidOrPhone) return \"\";\n return jidOrPhone.split(\"@\")[0].replace(/[^0-9]/g, \"\");\n}\n\nasync function getMic(deviceId?: string): Promise<MediaStream> {\n return navigator.mediaDevices.getUserMedia({\n audio: deviceId ? { deviceId: { exact: deviceId } } : true,\n });\n}\n\n/** A call (outgoing or accepted). */\nexport class Call {\n private media: Media | null = null;\n private mic: MediaStream | null = null;\n private accepted = false;\n private ended = false;\n private cbAccept: CB<Call>[] = [];\n private cbEnd: CB<{ reason?: string }>[] = [];\n private cbUnanswered: CB<void>[] = [];\n private cbState: CB<string>[] = [];\n\n constructor(\n private readonly client: Client,\n readonly sessionId: string,\n readonly callId: string,\n readonly peer: Peer,\n readonly direction: \"outbound\" | \"inbound\",\n ) {}\n\n onPeerAccept(cb: CB<Call>): this {\n this.cbAccept.push(cb);\n if (this.accepted) cb(this);\n return this;\n }\n onEnd(cb: CB<{ reason?: string }>): this {\n this.cbEnd.push(cb);\n return this;\n }\n onUnanswered(cb: CB<void>): this {\n this.cbUnanswered.push(cb);\n return this;\n }\n onStateChange(cb: CB<string>): this {\n this.cbState.push(cb);\n return this;\n }\n\n /** @internal */\n _attach(media: Media, mic: MediaStream | null): void {\n this.media = media;\n this.mic = mic;\n }\n /** @internal */\n _markAccepted(): void {\n if (this.accepted) return;\n this.accepted = true;\n this.cbAccept.forEach((c) => c(this));\n this.cbState.forEach((c) => c(\"accepted\"));\n }\n /** @internal */\n _markEnded(reason?: string): void {\n if (this.ended) return;\n this.ended = true;\n if (!this.accepted) this.cbUnanswered.forEach((c) => c());\n this.media?.close();\n this.mic?.getTracks().forEach((t) => t.stop());\n this.cbEnd.forEach((c) => c({ reason }));\n this.cbState.forEach((c) => c(\"ended\"));\n }\n\n mute(): void {\n this.mic?.getAudioTracks().forEach((t) => (t.enabled = false));\n }\n unmute(): void {\n this.mic?.getAudioTracks().forEach((t) => (t.enabled = true));\n }\n async end(): Promise<void> {\n if (this.ended) return;\n try {\n await this.client.del(`/api/sessions/${this.sessionId}/calls/${this.callId}`);\n } catch {\n /* server may already have ended it */\n }\n this._markEnded(\"user_ended\");\n }\n}\n\n/** An incoming call offer. */\nexport class Offer {\n constructor(\n private readonly dev: Device,\n readonly sessionId: string,\n readonly callId: string,\n readonly peer: Peer,\n ) {}\n\n accept(): Promise<Call> {\n return this.dev._answer(this.callId, this.peer);\n }\n async reject(): Promise<void> {\n await this.dev.client.post(`/api/sessions/${this.sessionId}/calls/${this.callId}/reject`);\n }\n}\n\n/** Internal per-token device: resolves its session, holds the SSE stream. */\nclass Device {\n sessionId = \"\";\n name = \"\";\n connected = false;\n es: EventSource | null = null;\n calls = new Map<string, Call>();\n\n constructor(\n readonly client: Client,\n private readonly parent: DigiCalls,\n ) {}\n\n async start(): Promise<void> {\n const { sessions } = await this.client.get<{\n sessions: { id: string; name?: string; state?: string; paired?: boolean }[];\n }>(\"/api/sessions\");\n const s = sessions?.[0];\n this.sessionId = s?.id ?? \"\";\n this.name = s?.name ?? \"\";\n this.connected = s?.state === \"open\";\n this.es = this.client.events((ev) => this.onEvent(ev));\n this.parent._emitUpdate();\n }\n\n stop(): void {\n this.es?.close();\n this.es = null;\n this.calls.forEach((c) => c._markEnded(\"closed\"));\n this.calls.clear();\n }\n\n private onEvent(ev: BrokerEvent): void {\n switch (ev.type) {\n case \"incoming\": {\n if (!ev.id) return;\n const offer = new Offer(this, ev.sessionId || this.sessionId, ev.id, { phone: phoneOf(ev.peer) });\n this.parent._emitOffer(offer);\n break;\n }\n case \"call-status\": {\n const c = ev.id ? this.calls.get(ev.id) : undefined;\n if (c && ev.status === \"connected\") c._markAccepted();\n break;\n }\n case \"call-ended\": {\n if (!ev.id) return;\n const c = this.calls.get(ev.id);\n if (c) {\n c._markEnded(ev.reason);\n this.calls.delete(ev.id);\n }\n break;\n }\n case \"auth-state\": {\n if (ev.sessionId === this.sessionId) {\n this.connected = ev.status === \"open\" || (ev as { state?: string }).state === \"open\";\n this.parent._emitUpdate();\n }\n break;\n }\n case \"call-peer\": {\n if (!ev.id) return;\n const e = ev as { peerPhone?: string; peerName?: string; peerAvatar?: string };\n this.parent._emitPeerInfo({ callId: ev.id, phone: e.peerPhone, name: e.peerName, avatar: e.peerAvatar });\n break;\n }\n }\n }\n\n /** @internal — accept an incoming call and bring up media. */\n async _answer(callId: string, peer: Peer): Promise<Call> {\n await this.client.post(`/api/sessions/${this.sessionId}/calls/${callId}/accept`);\n const mic = await getMic(this.parent.micDeviceId);\n const media = await negotiate(this.client, this.sessionId, callId, mic, this.parent.outputDeviceId, () => {\n this.calls.get(callId)?._markEnded(\"disconnected\");\n });\n const call = new Call(this.client, this.sessionId, callId, peer, \"inbound\");\n call._attach(media, mic);\n call._markAccepted();\n this.calls.set(callId, call);\n return call;\n }\n}\n\n/**\n * DigiCalls — WhatsApp calls SDK:\n * const dc = new DigiCalls({ baseUrl, tokens });\n * dc.onOffer(o => o.accept());\n * const { call, err } = await dc.startCall({ fromTokens, to });\n */\nexport class DigiCalls {\n micDeviceId?: string;\n outputDeviceId?: string;\n private devices = new Map<string, Device>();\n private offerCbs: CB<Offer>[] = [];\n private updateCbs: CB<void>[] = [];\n private peerInfoCbs: CB<PeerInfo>[] = [];\n\n constructor(private readonly config: DigiCallsConfig) {\n this.micDeviceId = config.micDeviceId;\n this.outputDeviceId = config.outputDeviceId;\n if (config.tokens?.length) void this.addDevices(config.tokens);\n }\n\n /** onUpdate fires when the device list or any device's connection changes. */\n onUpdate(cb: CB<void>): this {\n this.updateCbs.push(cb);\n return this;\n }\n /** @internal */\n _emitUpdate(): void {\n this.updateCbs.forEach((c) => c());\n }\n\n /** Devices currently registered in this instance (for a \"Números\" UI). */\n getDevices(): { token: string; name: string; connected: boolean }[] {\n return [...this.devices.entries()].map(([token, d]) => ({\n token,\n name: this.config.deviceNames?.[token] || d.name || token.slice(0, 10),\n connected: d.connected,\n }));\n }\n\n async addDevices(tokens: string[]): Promise<void> {\n for (const t of tokens) {\n if (this.devices.has(t)) continue;\n const dev = new Device(new Client(this.config.baseUrl, t), this);\n this.devices.set(t, dev);\n try {\n await dev.start();\n } catch {\n this.devices.delete(t);\n }\n }\n this._emitUpdate();\n }\n\n removeDevices(tokens: string[]): void {\n for (const t of tokens) {\n this.devices.get(t)?.stop();\n this.devices.delete(t);\n }\n this._emitUpdate();\n }\n\n onOffer(cb: CB<Offer>): this {\n this.offerCbs.push(cb);\n return this;\n }\n\n /** onPeerInfo fires when the backend resolves a call's contact (phone/name/avatar). */\n onPeerInfo(cb: CB<PeerInfo>): this {\n this.peerInfoCbs.push(cb);\n return this;\n }\n\n /** @internal */\n _emitOffer(o: Offer): void {\n this.offerCbs.forEach((c) => c(o));\n }\n /** @internal */\n _emitPeerInfo(info: PeerInfo): void {\n this.peerInfoCbs.forEach((c) => c(info));\n }\n\n async startCall({ to, fromTokens }: { to: string; fromTokens?: string[] }): Promise<{ call?: Call; err?: string }> {\n const token = fromTokens?.[0] ?? [...this.devices.keys()][0];\n const dev = token ? this.devices.get(token) : undefined;\n if (!dev || !dev.sessionId) return { err: \"no device available\" };\n try {\n const { call: created } = await dev.client.post<{ call: { callId: string } }>(\n `/api/sessions/${dev.sessionId}/calls`,\n { phone: to },\n );\n const callId = created.callId;\n const mic = await getMic(this.micDeviceId);\n const media = await negotiate(dev.client, dev.sessionId, callId, mic, this.outputDeviceId, () => {\n dev.calls.get(callId)?._markEnded(\"disconnected\");\n });\n const call = new Call(dev.client, dev.sessionId, callId, { phone: to }, \"outbound\");\n call._attach(media, mic);\n dev.calls.set(callId, call);\n return { call };\n } catch (e) {\n return { err: (e as Error).message };\n }\n }\n\n async getMultimediaDevices(): Promise<{ microphones: MediaDeviceInfo[]; speakers: MediaDeviceInfo[] }> {\n const list = await navigator.mediaDevices.enumerateDevices();\n return {\n microphones: list.filter((d) => d.kind === \"audioinput\"),\n speakers: list.filter((d) => d.kind === \"audiooutput\"),\n };\n }\n\n setMic(deviceId?: string): void {\n this.micDeviceId = deviceId;\n }\n setOutput(deviceId?: string): void {\n this.outputDeviceId = deviceId;\n }\n\n close(): void {\n this.devices.forEach((d) => d.stop());\n this.devices.clear();\n }\n}\n\nexport default DigiCalls;\n"],"names":["FALLBACK_ICE","Client","baseUrl","token","clientId","path","r","body","onEvent","u","es","e","iceServers","waitForIce","pc","timeoutMs","resolve","done","timer","onChange","negotiate","client","sessionId","callId","micStream","outputDeviceId","onClose","sendTx","track","remote","audioEl","stream","t","sink","sdp_answer","s","phoneOf","jidOrPhone","getMic","deviceId","Call","peer","direction","cb","media","mic","c","reason","Offer","dev","Device","parent","sessions","ev","offer","call","DigiCalls","config","d","tokens","o","info","to","fromTokens","created","list"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Client } from "./client";
|
|
2
|
+
import { type Media } from "./webrtc";
|
|
3
|
+
export interface Peer {
|
|
4
|
+
phone: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
avatar?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface PeerInfo {
|
|
9
|
+
callId: string;
|
|
10
|
+
phone?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
avatar?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface DigiCallsConfig {
|
|
15
|
+
/** DigiCalls backend, e.g. https://calls.digitalsac.io */
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
/** Device tokens (one per WhatsApp connection). */
|
|
18
|
+
tokens?: string[];
|
|
19
|
+
/** token -> friendly line name. */
|
|
20
|
+
deviceNames?: Record<string, string>;
|
|
21
|
+
micDeviceId?: string;
|
|
22
|
+
outputDeviceId?: string;
|
|
23
|
+
}
|
|
24
|
+
type CB<T> = (arg: T) => void;
|
|
25
|
+
/** A call (outgoing or accepted). */
|
|
26
|
+
export declare class Call {
|
|
27
|
+
private readonly client;
|
|
28
|
+
readonly sessionId: string;
|
|
29
|
+
readonly callId: string;
|
|
30
|
+
readonly peer: Peer;
|
|
31
|
+
readonly direction: "outbound" | "inbound";
|
|
32
|
+
private media;
|
|
33
|
+
private mic;
|
|
34
|
+
private accepted;
|
|
35
|
+
private ended;
|
|
36
|
+
private cbAccept;
|
|
37
|
+
private cbEnd;
|
|
38
|
+
private cbUnanswered;
|
|
39
|
+
private cbState;
|
|
40
|
+
constructor(client: Client, sessionId: string, callId: string, peer: Peer, direction: "outbound" | "inbound");
|
|
41
|
+
onPeerAccept(cb: CB<Call>): this;
|
|
42
|
+
onEnd(cb: CB<{
|
|
43
|
+
reason?: string;
|
|
44
|
+
}>): this;
|
|
45
|
+
onUnanswered(cb: CB<void>): this;
|
|
46
|
+
onStateChange(cb: CB<string>): this;
|
|
47
|
+
/** @internal */
|
|
48
|
+
_attach(media: Media, mic: MediaStream | null): void;
|
|
49
|
+
/** @internal */
|
|
50
|
+
_markAccepted(): void;
|
|
51
|
+
/** @internal */
|
|
52
|
+
_markEnded(reason?: string): void;
|
|
53
|
+
mute(): void;
|
|
54
|
+
unmute(): void;
|
|
55
|
+
end(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/** An incoming call offer. */
|
|
58
|
+
export declare class Offer {
|
|
59
|
+
private readonly dev;
|
|
60
|
+
readonly sessionId: string;
|
|
61
|
+
readonly callId: string;
|
|
62
|
+
readonly peer: Peer;
|
|
63
|
+
constructor(dev: Device, sessionId: string, callId: string, peer: Peer);
|
|
64
|
+
accept(): Promise<Call>;
|
|
65
|
+
reject(): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
/** Internal per-token device: resolves its session, holds the SSE stream. */
|
|
68
|
+
declare class Device {
|
|
69
|
+
readonly client: Client;
|
|
70
|
+
private readonly parent;
|
|
71
|
+
sessionId: string;
|
|
72
|
+
name: string;
|
|
73
|
+
connected: boolean;
|
|
74
|
+
es: EventSource | null;
|
|
75
|
+
calls: Map<string, Call>;
|
|
76
|
+
constructor(client: Client, parent: DigiCalls);
|
|
77
|
+
start(): Promise<void>;
|
|
78
|
+
stop(): void;
|
|
79
|
+
private onEvent;
|
|
80
|
+
/** @internal — accept an incoming call and bring up media. */
|
|
81
|
+
_answer(callId: string, peer: Peer): Promise<Call>;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* DigiCalls — WhatsApp calls SDK:
|
|
85
|
+
* const dc = new DigiCalls({ baseUrl, tokens });
|
|
86
|
+
* dc.onOffer(o => o.accept());
|
|
87
|
+
* const { call, err } = await dc.startCall({ fromTokens, to });
|
|
88
|
+
*/
|
|
89
|
+
export declare class DigiCalls {
|
|
90
|
+
private readonly config;
|
|
91
|
+
micDeviceId?: string;
|
|
92
|
+
outputDeviceId?: string;
|
|
93
|
+
private devices;
|
|
94
|
+
private offerCbs;
|
|
95
|
+
private updateCbs;
|
|
96
|
+
private peerInfoCbs;
|
|
97
|
+
constructor(config: DigiCallsConfig);
|
|
98
|
+
/** onUpdate fires when the device list or any device's connection changes. */
|
|
99
|
+
onUpdate(cb: CB<void>): this;
|
|
100
|
+
/** @internal */
|
|
101
|
+
_emitUpdate(): void;
|
|
102
|
+
/** Devices currently registered in this instance (for a "Números" UI). */
|
|
103
|
+
getDevices(): {
|
|
104
|
+
token: string;
|
|
105
|
+
name: string;
|
|
106
|
+
connected: boolean;
|
|
107
|
+
}[];
|
|
108
|
+
addDevices(tokens: string[]): Promise<void>;
|
|
109
|
+
removeDevices(tokens: string[]): void;
|
|
110
|
+
onOffer(cb: CB<Offer>): this;
|
|
111
|
+
/** onPeerInfo fires when the backend resolves a call's contact (phone/name/avatar). */
|
|
112
|
+
onPeerInfo(cb: CB<PeerInfo>): this;
|
|
113
|
+
/** @internal */
|
|
114
|
+
_emitOffer(o: Offer): void;
|
|
115
|
+
/** @internal */
|
|
116
|
+
_emitPeerInfo(info: PeerInfo): void;
|
|
117
|
+
startCall({ to, fromTokens }: {
|
|
118
|
+
to: string;
|
|
119
|
+
fromTokens?: string[];
|
|
120
|
+
}): Promise<{
|
|
121
|
+
call?: Call;
|
|
122
|
+
err?: string;
|
|
123
|
+
}>;
|
|
124
|
+
getMultimediaDevices(): Promise<{
|
|
125
|
+
microphones: MediaDeviceInfo[];
|
|
126
|
+
speakers: MediaDeviceInfo[];
|
|
127
|
+
}>;
|
|
128
|
+
setMic(deviceId?: string): void;
|
|
129
|
+
setOutput(deviceId?: string): void;
|
|
130
|
+
close(): void;
|
|
131
|
+
}
|
|
132
|
+
export default DigiCalls;
|
package/dist/webrtc.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Client } from "./client";
|
|
2
|
+
export type Media = {
|
|
3
|
+
pc: RTCPeerConnection;
|
|
4
|
+
remote: MediaStream;
|
|
5
|
+
close: () => void;
|
|
6
|
+
};
|
|
7
|
+
export declare function negotiate(client: Client, sessionId: string, callId: string, micStream: MediaStream | null, outputDeviceId: string | undefined, onClose: () => void): Promise<Media>;
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@digitalsac/digicalls-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DigiCalls SDK — motor de chamadas de WhatsApp por WebRTC, servido pelo seu backend DigiCalls. A UI fica em @digitalsac/digicalls-widget.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/digicalls.umd.cjs",
|
|
7
|
+
"module": "./dist/digicalls.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/digicalls.js",
|
|
13
|
+
"require": "./dist/digicalls.umd.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "vite build && tsc -p tsconfig.json",
|
|
22
|
+
"dev": "vite build --watch",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^5.6.0",
|
|
31
|
+
"vite": "^7.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|