zerorate-voip-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.
@@ -0,0 +1,1157 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ZerorateVoIPSDK = {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ class EventEmitter {
8
+ constructor() {
9
+ this.handlers = new Map();
10
+ }
11
+ on(event, callback) {
12
+ if (!this.handlers.has(event))
13
+ this.handlers.set(event, new Set());
14
+ this.handlers.get(event).add(callback);
15
+ }
16
+ off(event, callback) {
17
+ const set = this.handlers.get(event);
18
+ if (!set)
19
+ return;
20
+ set.delete(callback);
21
+ if (set.size === 0)
22
+ this.handlers.delete(event);
23
+ }
24
+ once(event, callback) {
25
+ const wrapper = (data) => {
26
+ this.off(event, wrapper);
27
+ callback(data);
28
+ };
29
+ this.on(event, wrapper);
30
+ }
31
+ emit(event, data) {
32
+ const set = this.handlers.get(event);
33
+ if (!set)
34
+ return;
35
+ for (const cb of Array.from(set))
36
+ cb(data);
37
+ }
38
+ }
39
+
40
+ class WebSocketManager extends EventEmitter {
41
+ constructor(wsUrl, authToken, autoReconnect = true, debug = false, userId) {
42
+ super();
43
+ this.state = "disconnected";
44
+ this.queue = [];
45
+ this.reconnectAttempts = 0;
46
+ this.url = wsUrl;
47
+ this.authToken = authToken;
48
+ this.autoReconnect = autoReconnect;
49
+ this.debug = debug;
50
+ this.userId = userId;
51
+ }
52
+ getState() {
53
+ return this.state;
54
+ }
55
+ isConnected() {
56
+ return this.state === "connected";
57
+ }
58
+ async connect() {
59
+ if (this.ws && (this.state === "connecting" || this.state === "connected"))
60
+ return;
61
+ this.state = "connecting";
62
+ try {
63
+ this.ws = new WebSocket(this.url);
64
+ this.ws.onopen = () => {
65
+ this.state = "connected";
66
+ this.emit("connected");
67
+ // Send authenticate handshake as JSON
68
+ if (this.userId && this.authToken) {
69
+ this.send({
70
+ type: "authenticate",
71
+ userId: this.userId,
72
+ token: this.authToken,
73
+ });
74
+ }
75
+ else if (this.authToken) {
76
+ this.send({ type: "authenticate", token: this.authToken });
77
+ }
78
+ this.flushQueue();
79
+ this.startHeartbeat();
80
+ this.reconnectAttempts = 0;
81
+ };
82
+ this.ws.onmessage = (ev) => {
83
+ if (typeof ev.data === "string") {
84
+ try {
85
+ const msg = JSON.parse(ev.data);
86
+ this.emit("message", msg);
87
+ }
88
+ catch (_e) {
89
+ this.emit("message", ev.data);
90
+ }
91
+ }
92
+ else {
93
+ this.emit("message", ev.data);
94
+ }
95
+ };
96
+ this.ws.onerror = () => {
97
+ this.state = "error";
98
+ this.emit("error", { type: "network" });
99
+ };
100
+ this.ws.onclose = () => {
101
+ this.stopHeartbeat();
102
+ const wasConnected = this.state === "connected";
103
+ this.state = "disconnected";
104
+ this.emit("disconnected");
105
+ if (this.autoReconnect) {
106
+ this.scheduleReconnect(wasConnected);
107
+ }
108
+ };
109
+ }
110
+ catch (_e) {
111
+ this.state = "error";
112
+ this.emit("error", { type: "network" });
113
+ }
114
+ }
115
+ disconnect() {
116
+ this.autoReconnect = false;
117
+ this.stopHeartbeat();
118
+ if (this.ws &&
119
+ (this.state === "connecting" || this.state === "connected")) {
120
+ try {
121
+ this.ws.close();
122
+ }
123
+ catch (_e) {
124
+ }
125
+ }
126
+ this.ws = undefined;
127
+ this.state = "disconnected";
128
+ this.emit("disconnected");
129
+ }
130
+ send(message) {
131
+ const payload = typeof message === "string" ? message : JSON.stringify(message);
132
+ if (this.ws && this.state === "connected") {
133
+ try {
134
+ this.ws.send(payload);
135
+ }
136
+ catch (_e) {
137
+ this.queue.push(payload);
138
+ }
139
+ }
140
+ else {
141
+ this.queue.push(payload);
142
+ }
143
+ }
144
+ flushQueue() {
145
+ if (!this.ws || this.state !== "connected")
146
+ return;
147
+ for (const m of this.queue.splice(0)) {
148
+ try {
149
+ this.ws.send(m);
150
+ }
151
+ catch (_e) {
152
+ }
153
+ }
154
+ }
155
+ startHeartbeat() {
156
+ this.stopHeartbeat();
157
+ this.heartbeatId = window.setInterval(() => {
158
+ if (!this.ws || this.state !== "connected")
159
+ return;
160
+ try {
161
+ this.ws.send(JSON.stringify({ type: "heartbeat" }));
162
+ if (this.pongTimeoutId)
163
+ window.clearTimeout(this.pongTimeoutId);
164
+ this.pongTimeoutId = window.setTimeout(() => {
165
+ try {
166
+ this.ws?.close();
167
+ }
168
+ catch (_e) {
169
+ void 0;
170
+ }
171
+ }, 5000);
172
+ }
173
+ catch (_e) {
174
+ }
175
+ }, 30000);
176
+ }
177
+ stopHeartbeat() {
178
+ if (this.heartbeatId) {
179
+ window.clearInterval(this.heartbeatId);
180
+ this.heartbeatId = undefined;
181
+ }
182
+ if (this.pongTimeoutId) {
183
+ window.clearTimeout(this.pongTimeoutId);
184
+ this.pongTimeoutId = undefined;
185
+ }
186
+ }
187
+ scheduleReconnect(wasConnected) {
188
+ this.reconnectAttempts += 1;
189
+ const base = wasConnected ? 1 : 2;
190
+ const delay = Math.min(30000, Math.pow(2, this.reconnectAttempts - 1) * 1000 * base);
191
+ this.emit("reconnecting", { attempt: this.reconnectAttempts, delay });
192
+ window.setTimeout(() => {
193
+ if (!this.autoReconnect)
194
+ return;
195
+ this.connect();
196
+ }, delay);
197
+ }
198
+ }
199
+
200
+ class APIClient extends EventEmitter {
201
+ constructor(baseUrl, authToken) {
202
+ super();
203
+ this.timeoutMs = 10000;
204
+ this.retries = 3;
205
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
206
+ this.authToken = authToken;
207
+ }
208
+ setAuthToken(token) {
209
+ this.authToken = token;
210
+ }
211
+ async initiateCall(data) {
212
+ return this.request("/api/webrtc/v1/calls/initiate", "POST", data);
213
+ }
214
+ async acceptCall(callId, userId) {
215
+ return this.request(`/api/webrtc/v1/calls/${encodeURIComponent(callId)}/accept`, "POST", { userId });
216
+ }
217
+ async declineCall(callId, userId) {
218
+ return this.request(`/api/webrtc/v1/calls/${encodeURIComponent(callId)}/decline`, "POST", { userId });
219
+ }
220
+ async endCall(callId, userId) {
221
+ return this.request(`/api/webrtc/v1/calls/${encodeURIComponent(callId)}/end`, "POST", { userId });
222
+ }
223
+ async getCallStatus(callId) {
224
+ return this.request(`/api/webrtc/v1/calls/${encodeURIComponent(callId)}`, "GET");
225
+ }
226
+ async request(path, method, body) {
227
+ const url = `${this.baseUrl}${path}`;
228
+ let attempt = 0;
229
+ let lastError;
230
+ while (attempt < this.retries) {
231
+ attempt += 1;
232
+ const controller = new AbortController();
233
+ const id = window.setTimeout(() => controller.abort(), this.timeoutMs);
234
+ try {
235
+ const res = await fetch(url, {
236
+ method,
237
+ headers: {
238
+ "Content-Type": "application/json",
239
+ ...(this.authToken
240
+ ? { Authorization: `Bearer ${this.authToken}` }
241
+ : {}),
242
+ },
243
+ body: body ? JSON.stringify(body) : undefined,
244
+ signal: controller.signal,
245
+ });
246
+ window.clearTimeout(id);
247
+ if (!res.ok) {
248
+ const errPayload = await this.safeJson(res);
249
+ lastError = { status: res.status, data: errPayload };
250
+ this.emit("error", {
251
+ type: "network",
252
+ message: "http_error",
253
+ originalError: lastError,
254
+ });
255
+ }
256
+ else {
257
+ const data = await this.safeJson(res);
258
+ return data;
259
+ }
260
+ }
261
+ catch (e) {
262
+ window.clearTimeout(id);
263
+ lastError = e;
264
+ this.emit("error", {
265
+ type: "network",
266
+ message: "fetch_error",
267
+ originalError: e,
268
+ });
269
+ }
270
+ }
271
+ throw lastError ?? new Error("request_failed");
272
+ }
273
+ async safeJson(res) {
274
+ try {
275
+ return await res.json();
276
+ }
277
+ catch (_e) {
278
+ return null;
279
+ }
280
+ }
281
+ }
282
+
283
+ class LiveKitManager extends EventEmitter {
284
+ async connectToRoom(livekitUrl, token, options) {
285
+ const mod = await import('livekit-client');
286
+ console.log(livekitUrl, token);
287
+ console.log(mod);
288
+ const Room = mod.Room;
289
+ const RoomEvent = mod.RoomEvent;
290
+ this.room = new Room(options);
291
+ await this.room.connect(livekitUrl, token);
292
+ this.room.on(RoomEvent.TrackSubscribed, (_track, _pub, participant) => {
293
+ this.emit("media:track-added", {
294
+ participantId: participant.sid,
295
+ trackType: _track.kind,
296
+ });
297
+ });
298
+ this.room.on(RoomEvent.TrackUnsubscribed, (_track, _pub, participant) => {
299
+ this.emit("media:track-removed", {
300
+ participantId: participant.sid,
301
+ trackType: _track.kind,
302
+ });
303
+ });
304
+ this.room.on(RoomEvent.Disconnected, () => {
305
+ this.emit("disconnected");
306
+ });
307
+ }
308
+ disconnectFromRoom() {
309
+ if (this.room) {
310
+ this.room.disconnect();
311
+ this.room = undefined;
312
+ }
313
+ }
314
+ async enableMicrophone() {
315
+ if (!this.room)
316
+ return;
317
+ await this.room.localParticipant.setMicrophoneEnabled(true);
318
+ this.emit("media:microphone-changed", { enabled: true });
319
+ }
320
+ async disableMicrophone() {
321
+ if (!this.room)
322
+ return;
323
+ await this.room.localParticipant.setMicrophoneEnabled(false);
324
+ this.emit("media:microphone-changed", { enabled: false });
325
+ }
326
+ async toggleMicrophone() {
327
+ if (!this.room)
328
+ return false;
329
+ const current = this.room.localParticipant.isMicrophoneEnabled;
330
+ await this.room.localParticipant.setMicrophoneEnabled(!current);
331
+ this.emit("media:microphone-changed", { enabled: !current });
332
+ return !current;
333
+ }
334
+ async enableVideo() {
335
+ if (!this.room)
336
+ return;
337
+ await this.room.localParticipant.setCameraEnabled(true);
338
+ this.emit("media:video-changed", { enabled: true });
339
+ }
340
+ async disableVideo() {
341
+ if (!this.room)
342
+ return;
343
+ await this.room.localParticipant.setCameraEnabled(false);
344
+ this.emit("media:video-changed", { enabled: false });
345
+ }
346
+ async toggleVideo() {
347
+ if (!this.room)
348
+ return false;
349
+ const current = this.room.localParticipant.isCameraEnabled;
350
+ await this.room.localParticipant.setCameraEnabled(!current);
351
+ this.emit("media:video-changed", { enabled: !current });
352
+ return !current;
353
+ }
354
+ getLocalTracks() {
355
+ if (!this.room)
356
+ return [];
357
+ return Array.from(this.room.localParticipant.tracks.values())
358
+ .map((p) => p.track)
359
+ .filter(Boolean);
360
+ }
361
+ getRemoteTracks() {
362
+ if (!this.room)
363
+ return [];
364
+ const tracks = [];
365
+ for (const p of this.room.participants.values()) {
366
+ for (const pub of p.tracks.values()) {
367
+ if (pub.track)
368
+ tracks.push(pub.track);
369
+ }
370
+ }
371
+ return tracks;
372
+ }
373
+ getRoom() {
374
+ return this.room;
375
+ }
376
+ attachTrack(track, element) {
377
+ if (!track)
378
+ return;
379
+ try {
380
+ track.attach(element);
381
+ }
382
+ catch (_e) {
383
+ }
384
+ }
385
+ detachTrack(track, element) {
386
+ if (!track)
387
+ return;
388
+ try {
389
+ track.detach(element);
390
+ }
391
+ catch (_e) {
392
+ }
393
+ }
394
+ }
395
+
396
+ class AudioManager {
397
+ constructor(ringTones) {
398
+ this.vol = 1;
399
+ this.playing = false;
400
+ if (ringTones?.incoming) {
401
+ this.incoming = new Audio(ringTones.incoming);
402
+ this.incoming.loop = true;
403
+ this.incoming.volume = this.vol;
404
+ }
405
+ if (ringTones?.outgoing) {
406
+ this.outgoing = new Audio(ringTones.outgoing);
407
+ this.outgoing.loop = true;
408
+ this.outgoing.volume = this.vol;
409
+ }
410
+ }
411
+ preloadRingtones() {
412
+ if (this.incoming)
413
+ this.incoming.load();
414
+ if (this.outgoing)
415
+ this.outgoing.load();
416
+ }
417
+ async playIncoming() {
418
+ if (!this.incoming)
419
+ return;
420
+ try {
421
+ await this.incoming.play();
422
+ this.playing = true;
423
+ }
424
+ catch (_e) {
425
+ this.playing = false;
426
+ }
427
+ }
428
+ async playOutgoing() {
429
+ if (!this.outgoing)
430
+ return;
431
+ try {
432
+ await this.outgoing.play();
433
+ this.playing = true;
434
+ }
435
+ catch (_e) {
436
+ this.playing = false;
437
+ }
438
+ }
439
+ stop() {
440
+ if (this.incoming) {
441
+ this.incoming.pause();
442
+ this.incoming.currentTime = 0;
443
+ }
444
+ if (this.outgoing) {
445
+ this.outgoing.pause();
446
+ this.outgoing.currentTime = 0;
447
+ }
448
+ this.playing = false;
449
+ }
450
+ setVolume(volume) {
451
+ this.vol = Math.max(0, Math.min(1, volume));
452
+ if (this.incoming)
453
+ this.incoming.volume = this.vol;
454
+ if (this.outgoing)
455
+ this.outgoing.volume = this.vol;
456
+ }
457
+ isPlaying() {
458
+ return this.playing;
459
+ }
460
+ }
461
+
462
+ class CallManager extends EventEmitter {
463
+ constructor(apiClient, webSocketManager, liveKitManager, audioManager, userId, userName) {
464
+ super();
465
+ this.state = "idle";
466
+ this.currentCall = null;
467
+ this.api = apiClient;
468
+ this.ws = webSocketManager;
469
+ this.livekit = liveKitManager;
470
+ this.audio = audioManager;
471
+ this.userId = userId;
472
+ this.userName = userName;
473
+ this.ws.on("message", (m) => this.handleSocketMessage(m));
474
+ }
475
+ getCallState() {
476
+ return this.state;
477
+ }
478
+ getCurrentCall() {
479
+ return this.currentCall;
480
+ }
481
+ async initiateCall(calleeId, calleeName, isVideo) {
482
+ if (this.state !== "idle")
483
+ return;
484
+ this.setState("calling");
485
+ await this.audio.playOutgoing();
486
+ try {
487
+ const res = await this.api.initiateCall({
488
+ callerId: this.userId,
489
+ callerName: this.userName,
490
+ calleeId,
491
+ calleeName,
492
+ isVideo,
493
+ });
494
+ const call = {
495
+ callId: res?.callId ?? "",
496
+ roomName: res?.roomName ?? "",
497
+ token: res?.token ?? "",
498
+ livekitUrl: res?.livekitUrl ?? "",
499
+ participantId: this.userId,
500
+ participantName: this.userName,
501
+ calleeId,
502
+ calleeName,
503
+ isVideo,
504
+ startTime: Date.now(),
505
+ };
506
+ console.log(call);
507
+ this.currentCall = call;
508
+ this.emit("call_started", {
509
+ callId: call.callId,
510
+ participants: [this.userId, calleeId],
511
+ });
512
+ this.setState("connecting");
513
+ await this.livekit.connectToRoom(call.livekitUrl, call.token, {
514
+ connect: true,
515
+ });
516
+ this.audio.stop();
517
+ this.setState("connected");
518
+ }
519
+ catch (e) {
520
+ this.audio.stop();
521
+ this.emit("call:error", { error: e });
522
+ this.setState("idle");
523
+ }
524
+ }
525
+ async acceptCall(callId) {
526
+ if (this.state !== "ringing")
527
+ return;
528
+ try {
529
+ await this.api.acceptCall(callId, this.userId);
530
+ this.setState("connecting");
531
+ }
532
+ catch (e) {
533
+ this.emit("call:error", { callId, error: e });
534
+ }
535
+ }
536
+ async declineCall(callId) {
537
+ if (this.state !== "ringing")
538
+ return;
539
+ try {
540
+ await this.api.declineCall(callId, this.userId);
541
+ this.audio.stop();
542
+ this.emit("call_declined", { callId });
543
+ this.cleanupCall();
544
+ }
545
+ catch (e) {
546
+ this.emit("call:error", { callId, error: e });
547
+ }
548
+ }
549
+ async endCall(callId) {
550
+ if (!this.currentCall || this.currentCall.callId !== callId)
551
+ return;
552
+ if (this.state !== "connected" && this.state !== "connecting")
553
+ return;
554
+ try {
555
+ await this.api.endCall(callId, this.userId);
556
+ }
557
+ catch (_e) {
558
+ }
559
+ this.finishCall();
560
+ }
561
+ async handleIncomingCall(data) {
562
+ if (this.state !== "idle")
563
+ return;
564
+ this.setState("ringing");
565
+ await this.audio.playIncoming();
566
+ const call = {
567
+ callId: data.callId,
568
+ roomName: data.roomName ?? "",
569
+ token: data.token ?? "",
570
+ livekitUrl: data.livekitUrl ?? "",
571
+ participantId: data.callerId,
572
+ participantName: data.callerName,
573
+ isVideo: !!data.isVideo,
574
+ };
575
+ this.currentCall = call;
576
+ this.emit("incoming_call", {
577
+ callId: call.callId,
578
+ callerId: call.participantId,
579
+ callerName: call.participantName,
580
+ isVideo: call.isVideo,
581
+ });
582
+ }
583
+ async handleAccepted() {
584
+ if (!this.currentCall)
585
+ return;
586
+ this.setState("connecting");
587
+ try {
588
+ await this.livekit.connectToRoom(this.currentCall.livekitUrl, this.currentCall.token, { connect: true });
589
+ this.audio.stop();
590
+ this.setState("connected");
591
+ this.emit("call_accepted", { callId: this.currentCall.callId });
592
+ }
593
+ catch (e) {
594
+ this.emit("call:error", { callId: this.currentCall.callId, error: e });
595
+ }
596
+ }
597
+ handleDeclined() {
598
+ if (!this.currentCall)
599
+ return;
600
+ this.audio.stop();
601
+ this.emit("call_declined", { callId: this.currentCall.callId });
602
+ this.cleanupCall();
603
+ }
604
+ handleMissed() {
605
+ if (!this.currentCall)
606
+ return;
607
+ this.audio.stop();
608
+ this.emit("call_missed", { callId: this.currentCall.callId });
609
+ this.cleanupCall();
610
+ }
611
+ handleEnded() {
612
+ if (!this.currentCall)
613
+ return;
614
+ this.finishCall();
615
+ }
616
+ handleSocketMessage(message) {
617
+ const type = message?.type;
618
+ if (type === "incoming_call")
619
+ this.handleIncomingCall(message.data ?? message);
620
+ if (type === "call_accepted")
621
+ this.handleAccepted();
622
+ if (type === "call_declined")
623
+ this.handleDeclined();
624
+ if (type === "call_missed")
625
+ this.handleMissed();
626
+ if (type === "call_ended")
627
+ this.handleEnded();
628
+ }
629
+ setState(next) {
630
+ const prev = this.state;
631
+ this.state = next;
632
+ this.emit("state:changed", { oldState: prev, newState: next });
633
+ }
634
+ finishCall() {
635
+ const callId = this.currentCall?.callId ?? "";
636
+ const start = this.currentCall?.startTime ?? Date.now();
637
+ const duration = Math.max(0, Math.floor((Date.now() - start) / 1000));
638
+ this.livekit.disconnectFromRoom();
639
+ this.emit("call_ended", { callId, duration });
640
+ this.cleanupCall();
641
+ }
642
+ cleanupCall() {
643
+ this.currentCall = null;
644
+ this.setState("idle");
645
+ }
646
+ }
647
+
648
+ function renderIncomingCall(client, data) {
649
+ const overlay = document.createElement("div");
650
+ overlay.className = "voip-overlay";
651
+ const card = document.createElement("div");
652
+ card.className = "voip-card";
653
+ const header = document.createElement("div");
654
+ header.className = "voip-header";
655
+ const avatar = document.createElement("div");
656
+ avatar.className = "voip-avatar";
657
+ const callerName = data?.callerName || "Unknown";
658
+ avatar.textContent = initials$2(callerName);
659
+ const meta = document.createElement("div");
660
+ meta.className = "voip-meta";
661
+ const title = document.createElement("div");
662
+ title.className = "voip-title";
663
+ title.textContent = data?.callerName ? `${data.callerName}` : "Incoming Call";
664
+ const subtitle = document.createElement("div");
665
+ subtitle.className = "voip-subtitle";
666
+ subtitle.textContent = data?.isVideo
667
+ ? "Incoming video call"
668
+ : "Incoming audio call";
669
+ meta.appendChild(title);
670
+ meta.appendChild(subtitle);
671
+ header.appendChild(avatar);
672
+ header.appendChild(meta);
673
+ const actions = document.createElement("div");
674
+ actions.className = "voip-actions";
675
+ const accept = document.createElement("button");
676
+ accept.className = "voip-btn voip-btn-accept";
677
+ accept.textContent = "Accept";
678
+ const decline = document.createElement("button");
679
+ decline.className = "voip-btn voip-btn-decline";
680
+ decline.textContent = "Decline";
681
+ const cleanup = () => {
682
+ window.removeEventListener("keydown", keyHandler);
683
+ };
684
+ const keyHandler = (e) => {
685
+ if (e.key === "Enter") {
686
+ if (data?.callId)
687
+ client.acceptCall(data.callId);
688
+ overlay.remove();
689
+ cleanup();
690
+ }
691
+ if (e.key === "Escape") {
692
+ if (data?.callId)
693
+ client.declineCall(data.callId);
694
+ overlay.remove();
695
+ cleanup();
696
+ }
697
+ };
698
+ window.addEventListener("keydown", keyHandler);
699
+ accept.onclick = () => {
700
+ if (data?.callId)
701
+ client.acceptCall(data.callId);
702
+ overlay.remove();
703
+ cleanup();
704
+ };
705
+ decline.onclick = () => {
706
+ if (data?.callId)
707
+ client.declineCall(data.callId);
708
+ overlay.remove();
709
+ cleanup();
710
+ };
711
+ actions.appendChild(accept);
712
+ actions.appendChild(decline);
713
+ card.appendChild(header);
714
+ card.appendChild(actions);
715
+ overlay.appendChild(card);
716
+ return overlay;
717
+ }
718
+ function initials$2(fullName) {
719
+ const parts = String(fullName).trim().split(/\s+/);
720
+ const first = parts[0]?.[0] || "";
721
+ const last = parts.length > 1 ? parts[parts.length - 1][0] : "";
722
+ return (first + last).toUpperCase() || "U";
723
+ }
724
+
725
+ function renderOutgoingCall(client, data) {
726
+ const overlay = document.createElement("div");
727
+ overlay.className = "voip-overlay";
728
+ const card = document.createElement("div");
729
+ card.className = "voip-card";
730
+ const header = document.createElement("div");
731
+ header.className = "voip-header";
732
+ const avatar = document.createElement("div");
733
+ avatar.className = "voip-avatar";
734
+ const name = data?.calleeName || "Unknown";
735
+ avatar.textContent = initials$1(name);
736
+ const meta = document.createElement("div");
737
+ meta.className = "voip-meta";
738
+ const title = document.createElement("div");
739
+ title.className = "voip-title";
740
+ title.textContent = data?.calleeName
741
+ ? `Calling ${data.calleeName}`
742
+ : "Calling";
743
+ const subtitle = document.createElement("div");
744
+ subtitle.className = "voip-subtitle";
745
+ subtitle.textContent = data?.isVideo ? "Video call" : "Audio call";
746
+ meta.appendChild(title);
747
+ meta.appendChild(subtitle);
748
+ header.appendChild(avatar);
749
+ header.appendChild(meta);
750
+ const actions = document.createElement("div");
751
+ actions.className = "voip-actions";
752
+ const cancel = document.createElement("button");
753
+ cancel.className = "voip-btn voip-btn-cancel";
754
+ cancel.textContent = "Cancel";
755
+ cancel.onclick = () => {
756
+ if (data?.callId)
757
+ client.endCall(data.callId);
758
+ overlay.remove();
759
+ };
760
+ actions.appendChild(cancel);
761
+ card.appendChild(header);
762
+ card.appendChild(actions);
763
+ overlay.appendChild(card);
764
+ return overlay;
765
+ }
766
+ function initials$1(fullName) {
767
+ const parts = String(fullName).trim().split(/\s+/);
768
+ const first = parts[0]?.[0] || "";
769
+ const last = parts.length > 1 ? parts[parts.length - 1][0] : "";
770
+ return (first + last).toUpperCase() || "U";
771
+ }
772
+
773
+ function renderActiveCall(client, data) {
774
+ const overlay = document.createElement("div");
775
+ overlay.className = "voip-overlay";
776
+ const card = document.createElement("div");
777
+ card.className = "voip-card";
778
+ // allow docking controls inside the card
779
+ card.style.position = "relative";
780
+ const header = document.createElement("div");
781
+ header.className = "voip-header";
782
+ const avatar = document.createElement("div");
783
+ avatar.className = "voip-avatar";
784
+ const displayName = data?.participantName ||
785
+ data?.calleeName ||
786
+ data?.callerName ||
787
+ "Participant";
788
+ avatar.textContent = initials(displayName);
789
+ const meta = document.createElement("div");
790
+ meta.className = "voip-meta";
791
+ const title = document.createElement("div");
792
+ title.className = "voip-title";
793
+ title.textContent = displayName;
794
+ const subtitle = document.createElement("div");
795
+ subtitle.className = "voip-subtitle";
796
+ const timer = document.createElement("span");
797
+ timer.className = "voip-timer";
798
+ timer.textContent = "00:00";
799
+ const start = typeof data?.startTime === "number" ? data.startTime : Date.now();
800
+ const updateTimer = () => {
801
+ const secs = Math.max(0, Math.floor((Date.now() - start) / 1000));
802
+ const m = String(Math.floor(secs / 60)).padStart(2, "0");
803
+ const s = String(secs % 60).padStart(2, "0");
804
+ timer.textContent = `${m}:${s}`;
805
+ };
806
+ const timerId = window.setInterval(updateTimer, 1000);
807
+ updateTimer();
808
+ subtitle.textContent = "In call • ";
809
+ subtitle.appendChild(timer);
810
+ meta.appendChild(title);
811
+ meta.appendChild(subtitle);
812
+ header.appendChild(avatar);
813
+ header.appendChild(meta);
814
+ // Layout container: responsive grid
815
+ const layout = document.createElement("div");
816
+ layout.style.display = "grid";
817
+ layout.style.gap = "16px";
818
+ const applyCols = () => {
819
+ layout.style.gridTemplateColumns =
820
+ window.innerWidth >= 768 ? "1fr 1fr" : "1fr";
821
+ };
822
+ applyCols();
823
+ const onResize = () => applyCols();
824
+ window.addEventListener("resize", onResize);
825
+ // Left: remote video container
826
+ const left = document.createElement("div");
827
+ const remoteContainer = document.createElement("div");
828
+ remoteContainer.className = "voip-video-container";
829
+ const remote = document.createElement("video");
830
+ remote.autoplay = true;
831
+ remote.playsInline = true;
832
+ remoteContainer.appendChild(remote);
833
+ left.appendChild(remoteContainer);
834
+ // Right: local preview panel
835
+ const right = document.createElement("div");
836
+ right.style.background = "#1f2937"; // gray-800
837
+ right.style.borderRadius = "12px";
838
+ right.style.padding = "16px";
839
+ right.style.height = "260px";
840
+ right.style.display = "flex";
841
+ right.style.alignItems = "center";
842
+ right.style.justifyContent = "center";
843
+ const local = document.createElement("video");
844
+ local.autoplay = true;
845
+ local.muted = true;
846
+ local.playsInline = true;
847
+ local.style.maxWidth = "100%";
848
+ local.style.maxHeight = "100%";
849
+ right.appendChild(local);
850
+ layout.appendChild(left);
851
+ layout.appendChild(right);
852
+ const controls = document.createElement("div");
853
+ controls.className = "voip-controls";
854
+ // dock controls bottom-center similar to the React implementation
855
+ controls.style.position = "absolute";
856
+ controls.style.left = "0";
857
+ controls.style.right = "0";
858
+ controls.style.bottom = "16px";
859
+ controls.style.display = "flex";
860
+ controls.style.justifyContent = "center";
861
+ controls.style.gap = "12px";
862
+ let micOn = true;
863
+ const mute = document.createElement("button");
864
+ mute.className = "voip-btn voip-btn-primary";
865
+ const setMicLabel = () => (mute.textContent = micOn ? "Mute" : "Unmute");
866
+ setMicLabel();
867
+ mute.onclick = async () => {
868
+ const next = await client.toggleMicrophone();
869
+ micOn = next;
870
+ setMicLabel();
871
+ };
872
+ let camOn = false;
873
+ const vid = document.createElement("button");
874
+ vid.className = "voip-btn voip-btn-primary";
875
+ const setCamLabel = () => (vid.textContent = camOn ? "Hide Video" : "Show Video");
876
+ setCamLabel();
877
+ vid.onclick = async () => {
878
+ const next = await client.toggleVideo();
879
+ camOn = next;
880
+ setCamLabel();
881
+ };
882
+ const end = document.createElement("button");
883
+ end.className = "voip-btn voip-btn-decline";
884
+ end.textContent = "End Call";
885
+ end.onclick = () => {
886
+ if (data?.callId)
887
+ client.endCall(data.callId);
888
+ overlay.remove();
889
+ window.clearInterval(timerId);
890
+ window.removeEventListener("resize", onResize);
891
+ };
892
+ controls.appendChild(mute);
893
+ controls.appendChild(vid);
894
+ controls.appendChild(end);
895
+ card.appendChild(header);
896
+ card.appendChild(layout);
897
+ card.appendChild(controls);
898
+ overlay.appendChild(card);
899
+ return overlay;
900
+ }
901
+ function initials(fullName) {
902
+ const parts = String(fullName).trim().split(/\s+/);
903
+ const first = parts[0]?.[0] || "";
904
+ const last = parts.length > 1 ? parts[parts.length - 1][0] : "";
905
+ return (first + last).toUpperCase() || "U";
906
+ }
907
+
908
+ class UIManager {
909
+ constructor(voipClient) {
910
+ this.client = voipClient;
911
+ }
912
+ show() {
913
+ this.ensureRoot();
914
+ }
915
+ hide() {
916
+ this.root?.remove();
917
+ this.root = undefined;
918
+ }
919
+ setTheme(theme) {
920
+ this.ensureRoot();
921
+ if (this.root) {
922
+ this.root.setAttribute("data-voip-theme", theme);
923
+ }
924
+ else {
925
+ document.documentElement.setAttribute("data-voip-theme", theme);
926
+ }
927
+ }
928
+ showIncomingCall(callData) {
929
+ this.ensureRoot();
930
+ const el = renderIncomingCall(this.client, callData);
931
+ this.root.appendChild(el);
932
+ }
933
+ showOutgoingCall(callData) {
934
+ this.ensureRoot();
935
+ const el = renderOutgoingCall(this.client, callData);
936
+ this.root.appendChild(el);
937
+ }
938
+ showActiveCall(callData) {
939
+ this.ensureRoot();
940
+ const el = renderActiveCall(this.client, callData);
941
+ this.root.appendChild(el);
942
+ const videos = el.querySelectorAll("video");
943
+ const remoteEl = videos[0];
944
+ const localEl = videos[1];
945
+ const remoteTracks = this.client.getRemoteTracks();
946
+ const localTracks = this.client.getLocalTracks();
947
+ const firstRemote = remoteTracks.find((t) => t?.kind === "video") ?? remoteTracks[0];
948
+ const firstLocal = localTracks.find((t) => t?.kind === "video") ?? localTracks[0];
949
+ if (firstRemote && remoteEl)
950
+ this.client.attachTrack(firstRemote, remoteEl);
951
+ if (firstLocal && localEl)
952
+ this.client.attachTrack(firstLocal, localEl);
953
+ }
954
+ hideAllModals() {
955
+ if (!this.root)
956
+ return;
957
+ while (this.root.firstChild)
958
+ this.root.removeChild(this.root.firstChild);
959
+ }
960
+ destroy() {
961
+ this.hide();
962
+ }
963
+ ensureRoot() {
964
+ if (this.root && document.body.contains(this.root))
965
+ return;
966
+ this.root = document.createElement("div");
967
+ document.body.appendChild(this.root);
968
+ }
969
+ }
970
+
971
+ class ZerorateVoIPClient {
972
+ constructor(config) {
973
+ this.emitter = new EventEmitter();
974
+ this.microphoneEnabled = true;
975
+ this.videoEnabled = false;
976
+ this.config = {
977
+ enableUI: true,
978
+ autoReconnect: true,
979
+ debug: false,
980
+ ...config,
981
+ };
982
+ this.ws = new WebSocketManager(this.config.wsUrl, this.config.authToken, !!this.config.autoReconnect, !!this.config.debug, this.config.userId);
983
+ this.api = new APIClient(this.config.backendUrl, this.config.authToken);
984
+ this.livekit = new LiveKitManager();
985
+ this.audio = new AudioManager(this.config.ringTones);
986
+ this.calls = new CallManager(this.api, this.ws, this.livekit, this.audio, this.config.userId, this.config.userName);
987
+ if (this.config.enableUI) {
988
+ this.ui = new UIManager(this);
989
+ }
990
+ this.bindEvents();
991
+ this.audio.preloadRingtones();
992
+ }
993
+ bindEvents() {
994
+ const forward = (e) => (data) => this.emitter.emit(e, data);
995
+ this.ws.on("connected", forward("connected"));
996
+ this.ws.on("disconnected", forward("disconnected"));
997
+ this.ws.on("reconnecting", forward("reconnecting"));
998
+ this.ws.on("error", forward("error"));
999
+ this.calls.on("state:changed", (payload) => {
1000
+ forward("state:changed")(payload);
1001
+ if (!this.ui)
1002
+ return;
1003
+ const state = payload?.newState;
1004
+ if (state === "calling" || state === "connecting") {
1005
+ this.ui.hideAllModals();
1006
+ this.ui.showOutgoingCall(this.calls.getCurrentCall() ?? {});
1007
+ }
1008
+ else if (state === "connected") {
1009
+ this.ui.hideAllModals();
1010
+ this.ui.showActiveCall(this.calls.getCurrentCall() ?? {});
1011
+ }
1012
+ else if (state === "idle" || state === "ending") {
1013
+ this.ui.hideAllModals();
1014
+ }
1015
+ });
1016
+ this.calls.on("incoming_call", (data) => {
1017
+ forward("incoming_call")(data);
1018
+ if (this.ui) {
1019
+ this.ui.hideAllModals();
1020
+ this.ui.showIncomingCall(data);
1021
+ }
1022
+ });
1023
+ this.calls.on("call_started", (data) => {
1024
+ forward("call_started")(data);
1025
+ });
1026
+ this.calls.on("call_accepted", (data) => {
1027
+ forward("call_accepted")(data);
1028
+ });
1029
+ this.calls.on("call_declined", (data) => {
1030
+ forward("call_declined")(data);
1031
+ if (this.ui)
1032
+ this.ui.hideAllModals();
1033
+ });
1034
+ this.calls.on("call_missed", (data) => {
1035
+ forward("call_missed")(data);
1036
+ if (this.ui)
1037
+ this.ui.hideAllModals();
1038
+ });
1039
+ this.calls.on("call_ended", (data) => {
1040
+ forward("call_ended")(data);
1041
+ if (this.ui)
1042
+ this.ui.hideAllModals();
1043
+ });
1044
+ this.livekit.on("media:microphone-changed", (d) => {
1045
+ this.microphoneEnabled = !!d?.enabled;
1046
+ this.emitter.emit("media:microphone-changed", d);
1047
+ });
1048
+ this.livekit.on("media:video-changed", (d) => {
1049
+ this.videoEnabled = !!d?.enabled;
1050
+ this.emitter.emit("media:video-changed", d);
1051
+ });
1052
+ }
1053
+ async connect() {
1054
+ await this.ws.connect();
1055
+ }
1056
+ disconnect() {
1057
+ this.ws.disconnect();
1058
+ }
1059
+ isConnected() {
1060
+ return this.ws.isConnected();
1061
+ }
1062
+ async initiateCall(userId, userName, options) {
1063
+ await this.calls.initiateCall(userId, userName, !!options?.isVideo);
1064
+ }
1065
+ async acceptCall(callId) {
1066
+ await this.calls.acceptCall(callId);
1067
+ }
1068
+ async declineCall(callId) {
1069
+ await this.calls.declineCall(callId);
1070
+ }
1071
+ async endCall(callId) {
1072
+ await this.calls.endCall(callId);
1073
+ }
1074
+ async toggleMicrophone() {
1075
+ const next = await this.livekit.toggleMicrophone();
1076
+ return next;
1077
+ }
1078
+ async toggleVideo() {
1079
+ const next = await this.livekit.toggleVideo();
1080
+ return next;
1081
+ }
1082
+ async setMicrophoneEnabled(enabled) {
1083
+ if (enabled)
1084
+ await this.livekit.enableMicrophone();
1085
+ else
1086
+ await this.livekit.disableMicrophone();
1087
+ }
1088
+ async setVideoEnabled(enabled) {
1089
+ if (enabled)
1090
+ await this.livekit.enableVideo();
1091
+ else
1092
+ await this.livekit.disableVideo();
1093
+ }
1094
+ getCallState() {
1095
+ return this.calls.getCallState();
1096
+ }
1097
+ getActiveCall() {
1098
+ return this.calls.getCurrentCall();
1099
+ }
1100
+ isInCall() {
1101
+ return (this.getCallState() === "connected" ||
1102
+ this.getCallState() === "connecting" ||
1103
+ this.getCallState() === "ringing" ||
1104
+ this.getCallState() === "calling");
1105
+ }
1106
+ getLocalTracks() {
1107
+ return this.livekit.getLocalTracks();
1108
+ }
1109
+ getRemoteTracks() {
1110
+ return this.livekit.getRemoteTracks();
1111
+ }
1112
+ attachTrack(track, element) {
1113
+ this.livekit.attachTrack(track, element);
1114
+ }
1115
+ detachTrack(track, element) {
1116
+ this.livekit.detachTrack(track, element);
1117
+ }
1118
+ on(event, callback) {
1119
+ this.emitter.on(event, callback);
1120
+ }
1121
+ off(event, callback) {
1122
+ this.emitter.off(event, callback);
1123
+ }
1124
+ once(event, callback) {
1125
+ this.emitter.once(event, callback);
1126
+ }
1127
+ showUI() {
1128
+ if (!this.ui) {
1129
+ this.ui = new UIManager(this);
1130
+ }
1131
+ this.ui.show();
1132
+ }
1133
+ hideUI() {
1134
+ this.ui?.hide();
1135
+ }
1136
+ setTheme(theme) {
1137
+ this.config.theme = theme;
1138
+ this.ui?.setTheme(theme);
1139
+ }
1140
+ destroy() {
1141
+ this.disconnect();
1142
+ this.hideUI();
1143
+ }
1144
+ }
1145
+
1146
+ exports.APIClient = APIClient;
1147
+ exports.AudioManager = AudioManager;
1148
+ exports.CallManager = CallManager;
1149
+ exports.EventEmitter = EventEmitter;
1150
+ exports.LiveKitManager = LiveKitManager;
1151
+ exports.WebSocketManager = WebSocketManager;
1152
+ exports.ZerorateVoIPClient = ZerorateVoIPClient;
1153
+ exports.default = ZerorateVoIPClient;
1154
+
1155
+ Object.defineProperty(exports, '__esModule', { value: true });
1156
+
1157
+ }));