@scalemule/conference 0.0.1

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +53 -0
  4. package/dist/assets/sounds.d.ts +12 -0
  5. package/dist/assets/sounds.d.ts.map +1 -0
  6. package/dist/core/ConferenceClient.d.ts +103 -0
  7. package/dist/core/ConferenceClient.d.ts.map +1 -0
  8. package/dist/index.cjs +171 -0
  9. package/dist/index.d.ts +5 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +168 -0
  12. package/dist/react/CallButton.d.ts +22 -0
  13. package/dist/react/CallButton.d.ts.map +1 -0
  14. package/dist/react/CallControls.d.ts +17 -0
  15. package/dist/react/CallControls.d.ts.map +1 -0
  16. package/dist/react/CallOverlay.d.ts +30 -0
  17. package/dist/react/CallOverlay.d.ts.map +1 -0
  18. package/dist/react/CallWindow.d.ts +10 -0
  19. package/dist/react/CallWindow.d.ts.map +1 -0
  20. package/dist/react/HuddleBar.d.ts +25 -0
  21. package/dist/react/HuddleBar.d.ts.map +1 -0
  22. package/dist/react/PreCallLobby.d.ts +22 -0
  23. package/dist/react/PreCallLobby.d.ts.map +1 -0
  24. package/dist/react/useConference.d.ts +37 -0
  25. package/dist/react/useConference.d.ts.map +1 -0
  26. package/dist/react/useDevices.d.ts +28 -0
  27. package/dist/react/useDevices.d.ts.map +1 -0
  28. package/dist/react/useIncomingCalls.d.ts +34 -0
  29. package/dist/react/useIncomingCalls.d.ts.map +1 -0
  30. package/dist/react.cjs +1004 -0
  31. package/dist/react.d.ts +18 -0
  32. package/dist/react.d.ts.map +1 -0
  33. package/dist/react.js +990 -0
  34. package/dist/transport.d.ts +21 -0
  35. package/dist/transport.d.ts.map +1 -0
  36. package/dist/types.d.ts +13 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/package.json +70 -0
package/dist/react.cjs ADDED
@@ -0,0 +1,1004 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/react/useConference.ts
7
+ var ConferenceContext = react.createContext(null);
8
+ function ConferenceProvider({ client, children }) {
9
+ const [activeSession, setActiveSession] = react.useState(null);
10
+ const [callState, setCallState] = react.useState("idle");
11
+ const refreshTimerRef = react.useRef(null);
12
+ const activeCallIdRef = react.useRef(null);
13
+ const clearRefreshTimer = react.useCallback(() => {
14
+ if (refreshTimerRef.current) {
15
+ clearTimeout(refreshTimerRef.current);
16
+ refreshTimerRef.current = null;
17
+ }
18
+ }, []);
19
+ const scheduleTokenRefresh = react.useCallback(
20
+ (session) => {
21
+ clearRefreshTimer();
22
+ const msUntilExpiry = session.tokenExpiresAt - Date.now();
23
+ const refreshIn = Math.max(msUntilExpiry - 3e4, 5e3);
24
+ refreshTimerRef.current = setTimeout(async () => {
25
+ if (!activeCallIdRef.current) return;
26
+ try {
27
+ const refreshed = await client.joinCall(activeCallIdRef.current);
28
+ setActiveSession(refreshed);
29
+ scheduleTokenRefresh(refreshed);
30
+ } catch {
31
+ setCallState("reconnecting");
32
+ }
33
+ }, refreshIn);
34
+ },
35
+ [client, clearRefreshTimer]
36
+ );
37
+ const join = react.useCallback(
38
+ async (callId) => {
39
+ setCallState("joining");
40
+ try {
41
+ const session = await client.joinCall(callId);
42
+ activeCallIdRef.current = callId;
43
+ setActiveSession(session);
44
+ setCallState("connected");
45
+ scheduleTokenRefresh(session);
46
+ } catch (err) {
47
+ setCallState("idle");
48
+ throw err;
49
+ }
50
+ },
51
+ [client, scheduleTokenRefresh]
52
+ );
53
+ const leave = react.useCallback(async () => {
54
+ clearRefreshTimer();
55
+ const callId = activeCallIdRef.current;
56
+ activeCallIdRef.current = null;
57
+ setActiveSession(null);
58
+ setCallState("idle");
59
+ if (callId) {
60
+ try {
61
+ await client.leaveCall(callId);
62
+ } catch {
63
+ }
64
+ }
65
+ }, [client, clearRefreshTimer]);
66
+ const end = react.useCallback(async () => {
67
+ clearRefreshTimer();
68
+ const callId = activeCallIdRef.current;
69
+ activeCallIdRef.current = null;
70
+ setActiveSession(null);
71
+ setCallState("ended");
72
+ if (callId) {
73
+ try {
74
+ await client.endCall(callId);
75
+ } catch {
76
+ }
77
+ }
78
+ }, [client, clearRefreshTimer]);
79
+ react.useEffect(() => {
80
+ return () => {
81
+ clearRefreshTimer();
82
+ };
83
+ }, [clearRefreshTimer]);
84
+ const value = {
85
+ client,
86
+ activeSession,
87
+ callState,
88
+ join,
89
+ leave,
90
+ end
91
+ };
92
+ return react.createElement(ConferenceContext.Provider, { value }, children);
93
+ }
94
+ function useConference() {
95
+ const ctx = react.useContext(ConferenceContext);
96
+ if (!ctx) {
97
+ throw new Error("useConference must be used within a <ConferenceProvider>");
98
+ }
99
+ return ctx;
100
+ }
101
+ function toDeviceInfo(d) {
102
+ return { deviceId: d.deviceId, label: d.label || `Device ${d.deviceId.slice(0, 8)}`, kind: d.kind };
103
+ }
104
+ function useDevices() {
105
+ const [audioInputs, setAudioInputs] = react.useState([]);
106
+ const [audioOutputs, setAudioOutputs] = react.useState([]);
107
+ const [videoInputs, setVideoInputs] = react.useState([]);
108
+ const [selectedAudioInput, setSelectedAudioInput] = react.useState(null);
109
+ const [selectedVideoInput, setSelectedVideoInput] = react.useState(null);
110
+ const [permissionState, setPermissionState] = react.useState("unknown");
111
+ const mountedRef = react.useRef(true);
112
+ const enumerate = react.useCallback(async () => {
113
+ if (typeof navigator === "undefined" || !navigator.mediaDevices) return;
114
+ try {
115
+ const devices = await navigator.mediaDevices.enumerateDevices();
116
+ if (!mountedRef.current) return;
117
+ const ai = devices.filter((d) => d.kind === "audioinput").map(toDeviceInfo);
118
+ const ao = devices.filter((d) => d.kind === "audiooutput").map(toDeviceInfo);
119
+ const vi = devices.filter((d) => d.kind === "videoinput").map(toDeviceInfo);
120
+ setAudioInputs(ai);
121
+ setAudioOutputs(ao);
122
+ setVideoInputs(vi);
123
+ const hasLabels = devices.some((d) => d.label !== "");
124
+ if (hasLabels) {
125
+ setPermissionState("granted");
126
+ }
127
+ } catch {
128
+ }
129
+ }, []);
130
+ react.useEffect(() => {
131
+ mountedRef.current = true;
132
+ enumerate();
133
+ if (typeof navigator !== "undefined" && navigator.permissions) {
134
+ navigator.permissions.query({ name: "microphone" }).then((result) => {
135
+ if (!mountedRef.current) return;
136
+ if (result.state === "granted") setPermissionState("granted");
137
+ else if (result.state === "denied") setPermissionState("denied");
138
+ else setPermissionState("prompt");
139
+ }).catch(() => {
140
+ if (mountedRef.current) setPermissionState("prompt");
141
+ });
142
+ }
143
+ const md = typeof navigator !== "undefined" ? navigator.mediaDevices : null;
144
+ if (md) {
145
+ md.addEventListener("devicechange", enumerate);
146
+ }
147
+ return () => {
148
+ mountedRef.current = false;
149
+ if (md) {
150
+ md.removeEventListener("devicechange", enumerate);
151
+ }
152
+ };
153
+ }, [enumerate]);
154
+ const requestPermission = react.useCallback(
155
+ async (constraints) => {
156
+ const stream = await navigator.mediaDevices.getUserMedia(
157
+ constraints ?? { audio: true, video: true }
158
+ );
159
+ if (mountedRef.current) {
160
+ setPermissionState("granted");
161
+ await enumerate();
162
+ }
163
+ return stream;
164
+ },
165
+ [enumerate]
166
+ );
167
+ return {
168
+ audioInputs,
169
+ audioOutputs,
170
+ videoInputs,
171
+ selectedAudioInput,
172
+ selectedVideoInput,
173
+ selectAudioInput: setSelectedAudioInput,
174
+ selectVideoInput: setSelectedVideoInput,
175
+ permissionState,
176
+ requestPermission
177
+ };
178
+ }
179
+ function useIncomingCalls(options) {
180
+ const {
181
+ client,
182
+ pollInterval = 5e3,
183
+ conversationIds,
184
+ currentUserId,
185
+ onNewCall,
186
+ disabled = false
187
+ } = options;
188
+ const [activeCalls, setActiveCalls] = react.useState([]);
189
+ const [isPolling, setIsPolling] = react.useState(false);
190
+ const seenCallIdsRef = react.useRef(/* @__PURE__ */ new Set());
191
+ const onNewCallRef = react.useRef(onNewCall);
192
+ onNewCallRef.current = onNewCall;
193
+ const poll = react.useCallback(async () => {
194
+ try {
195
+ const calls = await client.listCalls({ status: "active" });
196
+ let filtered = calls;
197
+ if (conversationIds && conversationIds.length > 0) {
198
+ const idSet = new Set(conversationIds);
199
+ filtered = calls.filter((c) => c.conversationId && idSet.has(c.conversationId));
200
+ }
201
+ if (currentUserId) {
202
+ filtered = filtered.filter((c) => c.createdBy !== currentUserId);
203
+ }
204
+ setActiveCalls(filtered);
205
+ for (const call of filtered) {
206
+ if (!seenCallIdsRef.current.has(call.id)) {
207
+ seenCallIdsRef.current.add(call.id);
208
+ onNewCallRef.current?.(call);
209
+ }
210
+ }
211
+ const activeIds = new Set(filtered.map((c) => c.id));
212
+ for (const id of seenCallIdsRef.current) {
213
+ if (!activeIds.has(id)) {
214
+ seenCallIdsRef.current.delete(id);
215
+ }
216
+ }
217
+ } catch {
218
+ }
219
+ }, [client, conversationIds, currentUserId]);
220
+ react.useEffect(() => {
221
+ if (disabled) {
222
+ setIsPolling(false);
223
+ return;
224
+ }
225
+ setIsPolling(true);
226
+ poll();
227
+ const interval = setInterval(() => {
228
+ if (typeof document !== "undefined" && document.hidden) return;
229
+ poll();
230
+ }, pollInterval);
231
+ return () => {
232
+ clearInterval(interval);
233
+ setIsPolling(false);
234
+ };
235
+ }, [poll, pollInterval, disabled]);
236
+ return { activeCalls, isPolling };
237
+ }
238
+ function CallButton({
239
+ conversationId,
240
+ callType = "video",
241
+ className,
242
+ style,
243
+ onCallStarted,
244
+ onError,
245
+ disabled = false,
246
+ children
247
+ }) {
248
+ const [isStarting, setIsStarting] = react.useState(false);
249
+ const handleClick = react.useCallback(async () => {
250
+ if (isStarting || disabled) return;
251
+ setIsStarting(true);
252
+ try {
253
+ const constraints = {
254
+ audio: true,
255
+ video: callType === "video"
256
+ };
257
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
258
+ stream.getTracks().forEach((t) => t.stop());
259
+ onCallStarted?.(conversationId);
260
+ } catch (err) {
261
+ const message = err instanceof Error ? err.message : "Failed to access media devices";
262
+ onError?.(message);
263
+ } finally {
264
+ setIsStarting(false);
265
+ }
266
+ }, [conversationId, callType, isStarting, disabled, onCallStarted, onError]);
267
+ const defaultStyle = {
268
+ display: "inline-flex",
269
+ alignItems: "center",
270
+ justifyContent: "center",
271
+ gap: 6,
272
+ padding: "6px 12px",
273
+ borderRadius: 8,
274
+ border: "none",
275
+ backgroundColor: "var(--sm-primary, #3b82f6)",
276
+ color: "#fff",
277
+ fontSize: 13,
278
+ fontWeight: 500,
279
+ cursor: disabled || isStarting ? "not-allowed" : "pointer",
280
+ opacity: disabled || isStarting ? 0.6 : 1,
281
+ transition: "opacity 0.15s",
282
+ ...style
283
+ };
284
+ const icon = callType === "audio" ? "\u{1F4DE}" : "\u{1F4F9}";
285
+ return /* @__PURE__ */ jsxRuntime.jsx(
286
+ "button",
287
+ {
288
+ type: "button",
289
+ className,
290
+ style: defaultStyle,
291
+ onClick: handleClick,
292
+ disabled: disabled || isStarting,
293
+ "aria-label": `Start ${callType} call`,
294
+ children: children || /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
295
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: icon }),
296
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: isStarting ? "Starting..." : callType === "audio" ? "Call" : "Video" })
297
+ ] })
298
+ }
299
+ );
300
+ }
301
+ function CallControls({
302
+ isMuted = false,
303
+ isVideoOff = false,
304
+ isScreenSharing = false,
305
+ onToggleMute,
306
+ onToggleVideo,
307
+ onToggleScreenShare,
308
+ onEndCall,
309
+ style
310
+ }) {
311
+ const barStyle = {
312
+ display: "flex",
313
+ gap: 12,
314
+ alignItems: "center",
315
+ justifyContent: "center",
316
+ padding: 12,
317
+ ...style
318
+ };
319
+ const btnBase = {
320
+ width: 48,
321
+ height: 48,
322
+ borderRadius: "50%",
323
+ border: "none",
324
+ cursor: "pointer",
325
+ fontSize: 18,
326
+ display: "flex",
327
+ alignItems: "center",
328
+ justifyContent: "center",
329
+ transition: "background-color 0.15s"
330
+ };
331
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: barStyle, children: [
332
+ /* @__PURE__ */ jsxRuntime.jsx(
333
+ "button",
334
+ {
335
+ type: "button",
336
+ onClick: onToggleMute,
337
+ style: { ...btnBase, backgroundColor: isMuted ? "#ef4444" : "#374151", color: "#fff" },
338
+ "aria-label": isMuted ? "Unmute" : "Mute",
339
+ title: isMuted ? "Unmute" : "Mute",
340
+ children: isMuted ? "\u{1F507}" : "\u{1F50A}"
341
+ }
342
+ ),
343
+ /* @__PURE__ */ jsxRuntime.jsx(
344
+ "button",
345
+ {
346
+ type: "button",
347
+ onClick: onToggleVideo,
348
+ style: { ...btnBase, backgroundColor: isVideoOff ? "#ef4444" : "#374151", color: "#fff" },
349
+ "aria-label": isVideoOff ? "Turn on camera" : "Turn off camera",
350
+ title: isVideoOff ? "Turn on camera" : "Turn off camera",
351
+ children: isVideoOff ? "\u{1F6AB}" : "\u{1F4F7}"
352
+ }
353
+ ),
354
+ /* @__PURE__ */ jsxRuntime.jsx(
355
+ "button",
356
+ {
357
+ type: "button",
358
+ onClick: onToggleScreenShare,
359
+ style: {
360
+ ...btnBase,
361
+ backgroundColor: isScreenSharing ? "#3b82f6" : "#374151",
362
+ color: "#fff"
363
+ },
364
+ "aria-label": isScreenSharing ? "Stop sharing" : "Share screen",
365
+ title: isScreenSharing ? "Stop sharing" : "Share screen",
366
+ children: "\u{1F5A5}"
367
+ }
368
+ ),
369
+ /* @__PURE__ */ jsxRuntime.jsx(
370
+ "button",
371
+ {
372
+ type: "button",
373
+ onClick: onEndCall,
374
+ style: { ...btnBase, backgroundColor: "#ef4444", color: "#fff", width: 56 },
375
+ "aria-label": "End call",
376
+ title: "End call",
377
+ children: "\u{1F4F5}"
378
+ }
379
+ )
380
+ ] });
381
+ }
382
+ var cachedComponents = null;
383
+ var importAttempted = false;
384
+ var importPromise = null;
385
+ function loadVideoBackendComponents() {
386
+ if (importPromise) return importPromise;
387
+ importPromise = import('@livekit/components-react').then((mod) => {
388
+ cachedComponents = {
389
+ Room: mod.LiveKitRoom,
390
+ VideoConference: mod.VideoConference,
391
+ RoomAudioRenderer: mod.RoomAudioRenderer
392
+ };
393
+ importAttempted = true;
394
+ return cachedComponents;
395
+ }).catch(() => {
396
+ importAttempted = true;
397
+ return null;
398
+ });
399
+ return importPromise;
400
+ }
401
+ function CallOverlay({
402
+ session,
403
+ onTokenRefresh,
404
+ onClose,
405
+ onError,
406
+ style
407
+ }) {
408
+ const [, setIsConnected] = react.useState(false);
409
+ const [backend, setBackend] = react.useState(cachedComponents);
410
+ const [backendLoaded, setBackendLoaded] = react.useState(importAttempted);
411
+ const [currentToken, setCurrentToken] = react.useState(session.accessToken);
412
+ const [currentServerUrl, setCurrentServerUrl] = react.useState(session.serverUrl);
413
+ react.useEffect(() => {
414
+ if (cachedComponents) {
415
+ setBackend(cachedComponents);
416
+ setBackendLoaded(true);
417
+ return;
418
+ }
419
+ let cancelled = false;
420
+ loadVideoBackendComponents().then((result) => {
421
+ if (!cancelled) {
422
+ setBackend(result);
423
+ setBackendLoaded(true);
424
+ }
425
+ });
426
+ return () => {
427
+ cancelled = true;
428
+ };
429
+ }, []);
430
+ react.useEffect(() => {
431
+ if (!onTokenRefresh) return;
432
+ const msUntilRefresh = Math.max(
433
+ 5e3,
434
+ session.tokenExpiresAt - Date.now() - 3e4
435
+ );
436
+ let cancelled = false;
437
+ const timer = setTimeout(async () => {
438
+ const backoffWindows = [
439
+ [1e3, 3e3],
440
+ [3e3, 1e4],
441
+ [1e4, 3e4]
442
+ ];
443
+ let lastError;
444
+ for (let attempt = 0; attempt <= backoffWindows.length; attempt++) {
445
+ if (cancelled) return;
446
+ try {
447
+ const next = await onTokenRefresh();
448
+ if (cancelled) return;
449
+ setCurrentToken(next.accessToken);
450
+ setCurrentServerUrl(next.serverUrl);
451
+ return;
452
+ } catch (err) {
453
+ lastError = err;
454
+ if (attempt >= backoffWindows.length) break;
455
+ const [minMs, maxMs] = backoffWindows[attempt];
456
+ const delayMs = minMs + Math.floor(Math.random() * (maxMs - minMs));
457
+ console.warn(
458
+ `[CallOverlay] Token refresh attempt ${attempt + 1} failed, retrying in ${delayMs}ms`,
459
+ err
460
+ );
461
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
462
+ }
463
+ }
464
+ if (cancelled) return;
465
+ const finalErr = lastError instanceof Error ? lastError : new Error("Token refresh failed after 4 attempts");
466
+ onError?.(finalErr);
467
+ }, msUntilRefresh);
468
+ return () => {
469
+ cancelled = true;
470
+ clearTimeout(timer);
471
+ };
472
+ }, [session.tokenExpiresAt, onTokenRefresh, onError]);
473
+ const overlayStyle = {
474
+ position: "fixed",
475
+ inset: 0,
476
+ backgroundColor: "#000",
477
+ display: "flex",
478
+ flexDirection: "column",
479
+ zIndex: 9999,
480
+ ...style
481
+ };
482
+ if (backend && currentToken && currentServerUrl) {
483
+ const Room = backend.Room;
484
+ const VideoConference = backend.VideoConference;
485
+ const RoomAudioRenderer = backend.RoomAudioRenderer;
486
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: overlayStyle, role: "dialog", "aria-label": "Video call", children: /* @__PURE__ */ jsxRuntime.jsxs(
487
+ Room,
488
+ {
489
+ serverUrl: currentServerUrl,
490
+ token: currentToken,
491
+ connect: true,
492
+ onConnected: () => setIsConnected(true),
493
+ onDisconnected: () => setIsConnected(false),
494
+ onError: (err) => {
495
+ onError?.(err);
496
+ },
497
+ options: {
498
+ adaptiveStream: true,
499
+ dynacast: true
500
+ },
501
+ children: [
502
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { flex: 1, position: "relative" }, children: [
503
+ /* @__PURE__ */ jsxRuntime.jsx(VideoConference, {}),
504
+ /* @__PURE__ */ jsxRuntime.jsx(RoomAudioRenderer, {})
505
+ ] }),
506
+ /* @__PURE__ */ jsxRuntime.jsx(
507
+ "div",
508
+ {
509
+ style: {
510
+ position: "absolute",
511
+ top: 12,
512
+ right: 12,
513
+ zIndex: 1e4
514
+ },
515
+ children: /* @__PURE__ */ jsxRuntime.jsx(
516
+ "button",
517
+ {
518
+ type: "button",
519
+ onClick: onClose,
520
+ style: {
521
+ padding: "8px 20px",
522
+ borderRadius: 20,
523
+ border: "none",
524
+ backgroundColor: "#ef4444",
525
+ color: "#fff",
526
+ fontSize: 13,
527
+ fontWeight: 600,
528
+ cursor: "pointer"
529
+ },
530
+ children: "End Call"
531
+ }
532
+ )
533
+ }
534
+ )
535
+ ]
536
+ }
537
+ ) });
538
+ }
539
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: overlayStyle, role: "dialog", "aria-label": "Video call", children: /* @__PURE__ */ jsxRuntime.jsxs(
540
+ "div",
541
+ {
542
+ style: {
543
+ flex: 1,
544
+ display: "flex",
545
+ flexDirection: "column",
546
+ alignItems: "center",
547
+ justifyContent: "center",
548
+ color: "#fff"
549
+ },
550
+ children: [
551
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: 18, marginBottom: 8 }, children: !backendLoaded ? "Loading..." : currentServerUrl ? "Connecting..." : "Video conferencing not configured" }),
552
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { fontSize: 12, color: "#9ca3af", marginBottom: 24 }, children: [
553
+ "Call ID: ",
554
+ session.callId
555
+ ] }),
556
+ backendLoaded && !backend && /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { fontSize: 13, color: "#d1d5db", maxWidth: 400, textAlign: "center" }, children: [
557
+ "Video backend failed to load. Reinstall ",
558
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "@scalemule/conference" }),
559
+ " to repair."
560
+ ] }),
561
+ onClose && /* @__PURE__ */ jsxRuntime.jsx(
562
+ "button",
563
+ {
564
+ type: "button",
565
+ onClick: onClose,
566
+ style: {
567
+ padding: "10px 24px",
568
+ borderRadius: 24,
569
+ border: "none",
570
+ backgroundColor: "#ef4444",
571
+ color: "#fff",
572
+ fontSize: 14,
573
+ fontWeight: 600,
574
+ cursor: "pointer",
575
+ marginTop: 16
576
+ },
577
+ children: "End Call"
578
+ }
579
+ )
580
+ ]
581
+ }
582
+ ) });
583
+ }
584
+ function PreCallLobby({
585
+ callType = "video",
586
+ onJoin,
587
+ onCancel,
588
+ style,
589
+ className
590
+ }) {
591
+ const [state, setState] = react.useState("checking");
592
+ const [errorMessage, setErrorMessage] = react.useState(null);
593
+ const [stream, setStream] = react.useState(null);
594
+ const videoRef = react.useRef(null);
595
+ const {
596
+ audioInputs,
597
+ videoInputs,
598
+ selectedAudioInput,
599
+ selectedVideoInput,
600
+ selectAudioInput,
601
+ selectVideoInput,
602
+ permissionState,
603
+ requestPermission
604
+ } = useDevices();
605
+ react.useEffect(() => {
606
+ if (permissionState === "granted") {
607
+ setState("prompt");
608
+ } else if (permissionState === "denied") {
609
+ setState("denied");
610
+ } else if (permissionState !== "unknown") {
611
+ setState("prompt");
612
+ }
613
+ }, [permissionState]);
614
+ react.useEffect(() => {
615
+ if (videoRef.current && stream) {
616
+ videoRef.current.srcObject = stream;
617
+ }
618
+ }, [stream]);
619
+ react.useEffect(() => {
620
+ return () => {
621
+ if (stream) {
622
+ stream.getTracks().forEach((t) => t.stop());
623
+ }
624
+ };
625
+ }, [stream]);
626
+ const acquireMedia = react.useCallback(async () => {
627
+ setState("acquiring");
628
+ try {
629
+ const constraints = {
630
+ audio: selectedAudioInput ? { deviceId: { exact: selectedAudioInput } } : true,
631
+ video: callType === "video" ? selectedVideoInput ? { deviceId: { exact: selectedVideoInput } } : true : false
632
+ };
633
+ const s = await requestPermission(constraints);
634
+ setStream(s);
635
+ setState("preview");
636
+ } catch (err) {
637
+ const e = err;
638
+ if (e.name === "NotAllowedError") {
639
+ setState("denied");
640
+ } else if (e.name === "NotFoundError") {
641
+ setErrorMessage("No camera or microphone found");
642
+ setState("error");
643
+ } else if (e.name === "NotReadableError") {
644
+ setErrorMessage("Device is in use by another application");
645
+ setState("error");
646
+ } else {
647
+ setErrorMessage(e.message || "Failed to access media devices");
648
+ setState("error");
649
+ }
650
+ }
651
+ }, [callType, selectedAudioInput, selectedVideoInput, requestPermission]);
652
+ const handleJoin = react.useCallback(() => {
653
+ if (stream) {
654
+ stream.getTracks().forEach((t) => t.stop());
655
+ setStream(null);
656
+ }
657
+ onJoin({
658
+ audio: true,
659
+ video: callType === "video",
660
+ audioDeviceId: selectedAudioInput ?? void 0,
661
+ videoDeviceId: selectedVideoInput ?? void 0
662
+ });
663
+ }, [stream, onJoin, callType, selectedAudioInput, selectedVideoInput]);
664
+ const containerStyle = {
665
+ display: "flex",
666
+ flexDirection: "column",
667
+ alignItems: "center",
668
+ gap: "16px",
669
+ padding: "24px",
670
+ background: "#1a1a2e",
671
+ borderRadius: "12px",
672
+ color: "#fff",
673
+ maxWidth: "400px",
674
+ margin: "0 auto",
675
+ ...style
676
+ };
677
+ const buttonStyle = {
678
+ padding: "10px 24px",
679
+ borderRadius: "8px",
680
+ border: "none",
681
+ cursor: "pointer",
682
+ fontSize: "14px",
683
+ fontWeight: 600
684
+ };
685
+ const primaryButton = {
686
+ ...buttonStyle,
687
+ background: "#4CAF50",
688
+ color: "#fff"
689
+ };
690
+ const secondaryButton = {
691
+ ...buttonStyle,
692
+ background: "transparent",
693
+ color: "#aaa",
694
+ border: "1px solid #444"
695
+ };
696
+ const selectStyle = {
697
+ width: "100%",
698
+ padding: "8px",
699
+ borderRadius: "6px",
700
+ background: "#2a2a4a",
701
+ color: "#fff",
702
+ border: "1px solid #444",
703
+ fontSize: "13px"
704
+ };
705
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: containerStyle, className, children: [
706
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { style: { margin: 0, fontSize: "18px" }, children: callType === "video" ? "Video Call" : "Audio Call" }),
707
+ state === "checking" && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#888" }, children: "Checking permissions..." }),
708
+ state === "prompt" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
709
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { color: "#ccc", textAlign: "center", fontSize: "14px" }, children: [
710
+ "Allow access to your ",
711
+ callType === "video" ? "camera and microphone" : "microphone",
712
+ " to join the call."
713
+ ] }),
714
+ /* @__PURE__ */ jsxRuntime.jsx("button", { style: primaryButton, onClick: acquireMedia, children: "Allow & Preview" })
715
+ ] }),
716
+ state === "acquiring" && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#888" }, children: "Requesting access..." }),
717
+ state === "preview" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
718
+ callType === "video" && /* @__PURE__ */ jsxRuntime.jsx(
719
+ "video",
720
+ {
721
+ ref: videoRef,
722
+ autoPlay: true,
723
+ playsInline: true,
724
+ muted: true,
725
+ style: {
726
+ width: "100%",
727
+ maxHeight: "240px",
728
+ borderRadius: "8px",
729
+ background: "#000",
730
+ objectFit: "cover"
731
+ }
732
+ }
733
+ ),
734
+ callType === "audio" && /* @__PURE__ */ jsxRuntime.jsxs(
735
+ "div",
736
+ {
737
+ style: {
738
+ display: "flex",
739
+ alignItems: "center",
740
+ gap: "8px",
741
+ padding: "16px",
742
+ background: "#2a2a4a",
743
+ borderRadius: "8px",
744
+ width: "100%"
745
+ },
746
+ children: [
747
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "20px" }, children: "\u{1F3A4}" }),
748
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#4CAF50", fontSize: "14px" }, children: "Microphone ready" })
749
+ ]
750
+ }
751
+ ),
752
+ audioInputs.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(
753
+ "select",
754
+ {
755
+ style: selectStyle,
756
+ value: selectedAudioInput ?? "",
757
+ onChange: (e) => selectAudioInput(e.target.value),
758
+ "aria-label": "Select microphone",
759
+ children: audioInputs.map((d) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: d.deviceId, children: d.label }, d.deviceId))
760
+ }
761
+ ),
762
+ callType === "video" && videoInputs.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(
763
+ "select",
764
+ {
765
+ style: selectStyle,
766
+ value: selectedVideoInput ?? "",
767
+ onChange: (e) => selectVideoInput(e.target.value),
768
+ "aria-label": "Select camera",
769
+ children: videoInputs.map((d) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: d.deviceId, children: d.label }, d.deviceId))
770
+ }
771
+ ),
772
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "12px" }, children: [
773
+ /* @__PURE__ */ jsxRuntime.jsx("button", { style: primaryButton, onClick: handleJoin, children: callType === "video" ? "Join with Video" : "Join with Audio" }),
774
+ onCancel && /* @__PURE__ */ jsxRuntime.jsx("button", { style: secondaryButton, onClick: onCancel, children: "Cancel" })
775
+ ] })
776
+ ] }),
777
+ state === "denied" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
778
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#ff6b6b", textAlign: "center", fontSize: "14px" }, children: "Permission denied. Please allow access in your browser settings." }),
779
+ onCancel && /* @__PURE__ */ jsxRuntime.jsx("button", { style: secondaryButton, onClick: onCancel, children: "Cancel" })
780
+ ] }),
781
+ state === "error" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
782
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#ff6b6b", textAlign: "center", fontSize: "14px" }, children: errorMessage || "An error occurred" }),
783
+ /* @__PURE__ */ jsxRuntime.jsx("button", { style: secondaryButton, onClick: acquireMedia, children: "Retry" }),
784
+ onCancel && /* @__PURE__ */ jsxRuntime.jsx("button", { style: secondaryButton, onClick: onCancel, children: "Cancel" })
785
+ ] })
786
+ ] });
787
+ }
788
+ var lkComponents = null;
789
+ var lkPromise = null;
790
+ function loadLiveKit() {
791
+ if (lkPromise) return lkPromise;
792
+ lkPromise = import('@livekit/components-react').then((mod) => {
793
+ lkComponents = {
794
+ LiveKitRoom: mod.LiveKitRoom,
795
+ RoomAudioRenderer: mod.RoomAudioRenderer,
796
+ TrackToggle: mod.TrackToggle,
797
+ DisconnectButton: mod.DisconnectButton
798
+ };
799
+ return lkComponents;
800
+ }).catch(() => null);
801
+ return lkPromise;
802
+ }
803
+ function formatDuration(seconds) {
804
+ const m = Math.floor(seconds / 60);
805
+ const s = seconds % 60;
806
+ return `${m}:${s.toString().padStart(2, "0")}`;
807
+ }
808
+ function HuddleBar({
809
+ session,
810
+ onClose,
811
+ onError,
812
+ onTokenRefresh,
813
+ callType = "audio",
814
+ participantNames,
815
+ style,
816
+ className
817
+ }) {
818
+ const [components, setComponents] = react.useState(lkComponents);
819
+ const [connectionState, setConnectionState] = react.useState("connecting");
820
+ const [elapsed, setElapsed] = react.useState(0);
821
+ const [token, setToken] = react.useState(session.accessToken);
822
+ const startTimeRef = react.useRef(Date.now());
823
+ const refreshTimerRef = react.useRef(null);
824
+ react.useEffect(() => {
825
+ if (!lkComponents) {
826
+ loadLiveKit().then((c) => {
827
+ if (c) setComponents(c);
828
+ });
829
+ }
830
+ }, []);
831
+ react.useEffect(() => {
832
+ const interval = setInterval(() => {
833
+ setElapsed(Math.floor((Date.now() - startTimeRef.current) / 1e3));
834
+ }, 1e3);
835
+ return () => clearInterval(interval);
836
+ }, []);
837
+ react.useEffect(() => {
838
+ if (!onTokenRefresh) return;
839
+ const msUntilExpiry = session.tokenExpiresAt - Date.now();
840
+ const refreshIn = Math.max(msUntilExpiry - 3e4, 5e3);
841
+ refreshTimerRef.current = setTimeout(async () => {
842
+ try {
843
+ const refreshed = await onTokenRefresh();
844
+ setToken(refreshed.accessToken);
845
+ } catch {
846
+ setConnectionState("reconnecting");
847
+ }
848
+ }, refreshIn);
849
+ return () => {
850
+ if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
851
+ };
852
+ }, [session.tokenExpiresAt, onTokenRefresh]);
853
+ react.useEffect(() => {
854
+ const t = setTimeout(() => setConnectionState("connected"), 1500);
855
+ return () => clearTimeout(t);
856
+ }, []);
857
+ const barStyle = react.useMemo(
858
+ () => ({
859
+ position: "fixed",
860
+ bottom: 0,
861
+ left: 0,
862
+ right: 0,
863
+ height: "56px",
864
+ background: "linear-gradient(180deg, #2a2a3e 0%, #1e1e30 100%)",
865
+ display: "flex",
866
+ alignItems: "center",
867
+ justifyContent: "space-between",
868
+ padding: "0 16px",
869
+ zIndex: 9998,
870
+ color: "#fff",
871
+ fontSize: "13px",
872
+ borderTop: "1px solid #3a3a5a",
873
+ ...style
874
+ }),
875
+ [style]
876
+ );
877
+ const statusDotStyle = {
878
+ width: "8px",
879
+ height: "8px",
880
+ borderRadius: "50%",
881
+ background: connectionState === "connected" ? "#4CAF50" : "#FFC107",
882
+ animation: connectionState === "connecting" ? "pulse 1.5s infinite" : void 0
883
+ };
884
+ const controlButtonStyle = {
885
+ background: "rgba(255,255,255,0.1)",
886
+ border: "none",
887
+ borderRadius: "50%",
888
+ width: "36px",
889
+ height: "36px",
890
+ display: "flex",
891
+ alignItems: "center",
892
+ justifyContent: "center",
893
+ cursor: "pointer",
894
+ color: "#fff",
895
+ fontSize: "16px"
896
+ };
897
+ const leaveButtonStyle = {
898
+ ...controlButtonStyle,
899
+ background: "#e53935",
900
+ borderRadius: "18px",
901
+ width: "auto",
902
+ padding: "0 16px",
903
+ fontSize: "13px",
904
+ fontWeight: 600
905
+ };
906
+ if (!components) {
907
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: barStyle, className, role: "toolbar", "aria-label": "Call controls", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
908
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: statusDotStyle }),
909
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Connecting..." })
910
+ ] }) });
911
+ }
912
+ const { LiveKitRoom, RoomAudioRenderer, TrackToggle, DisconnectButton } = components;
913
+ return /* @__PURE__ */ jsxRuntime.jsxs(
914
+ LiveKitRoom,
915
+ {
916
+ serverUrl: session.serverUrl,
917
+ token,
918
+ connect: true,
919
+ audio: callType === "audio" || callType === "video",
920
+ video: callType === "video",
921
+ onDisconnected: () => onClose?.(),
922
+ onError: (err) => onError?.(err),
923
+ children: [
924
+ /* @__PURE__ */ jsxRuntime.jsx(RoomAudioRenderer, {}),
925
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: barStyle, className, role: "toolbar", "aria-label": "Call controls", children: [
926
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
927
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: statusDotStyle }),
928
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: callType === "video" ? "Video call" : "Audio call" }),
929
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#888" }, children: formatDuration(elapsed) })
930
+ ] }),
931
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
932
+ /* @__PURE__ */ jsxRuntime.jsx(TrackToggle, { source: "microphone", style: controlButtonStyle }),
933
+ callType === "video" && /* @__PURE__ */ jsxRuntime.jsx(TrackToggle, { source: "camera", style: controlButtonStyle }),
934
+ /* @__PURE__ */ jsxRuntime.jsx(DisconnectButton, { style: leaveButtonStyle, children: "Leave" })
935
+ ] })
936
+ ] })
937
+ ]
938
+ }
939
+ );
940
+ }
941
+
942
+ // src/react/CallWindow.tsx
943
+ function CallWindow() {
944
+ return null;
945
+ }
946
+
947
+ // src/assets/sounds.ts
948
+ function generateTone(frequency, durationMs, sampleRate = 8e3) {
949
+ const numSamples = Math.floor(sampleRate * (durationMs / 1e3));
950
+ const dataSize = numSamples;
951
+ const fileSize = 44 + dataSize;
952
+ const buffer = new ArrayBuffer(fileSize);
953
+ const view = new DataView(buffer);
954
+ const writeString = (offset, str) => {
955
+ for (let i = 0; i < str.length; i++) view.setUint8(offset + i, str.charCodeAt(i));
956
+ };
957
+ writeString(0, "RIFF");
958
+ view.setUint32(4, fileSize - 8, true);
959
+ writeString(8, "WAVE");
960
+ writeString(12, "fmt ");
961
+ view.setUint32(16, 16, true);
962
+ view.setUint16(20, 1, true);
963
+ view.setUint16(22, 1, true);
964
+ view.setUint32(24, sampleRate, true);
965
+ view.setUint32(28, sampleRate, true);
966
+ view.setUint16(32, 1, true);
967
+ view.setUint16(34, 8, true);
968
+ writeString(36, "data");
969
+ view.setUint32(40, dataSize, true);
970
+ const fadeLength = Math.floor(numSamples * 0.1);
971
+ for (let i = 0; i < numSamples; i++) {
972
+ let amplitude = Math.sin(2 * Math.PI * frequency * i / sampleRate);
973
+ if (i < fadeLength) amplitude *= i / fadeLength;
974
+ if (i > numSamples - fadeLength) amplitude *= (numSamples - i) / fadeLength;
975
+ view.setUint8(44 + i, Math.floor(128 + amplitude * 64));
976
+ }
977
+ const bytes = new Uint8Array(buffer);
978
+ let binary = "";
979
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
980
+ return "data:audio/wav;base64," + btoa(binary);
981
+ }
982
+ var JOIN_SOUND = generateTone(880, 200);
983
+ var RING_SOUND = generateTone(587, 400);
984
+ function playSound(sound) {
985
+ const src = sound === "join" ? JOIN_SOUND : RING_SOUND;
986
+ const audio = new Audio(src);
987
+ audio.volume = 0.5;
988
+ return audio.play().catch(() => {
989
+ });
990
+ }
991
+
992
+ exports.CallButton = CallButton;
993
+ exports.CallControls = CallControls;
994
+ exports.CallOverlay = CallOverlay;
995
+ exports.CallWindow = CallWindow;
996
+ exports.ConferenceProvider = ConferenceProvider;
997
+ exports.HuddleBar = HuddleBar;
998
+ exports.JOIN_SOUND = JOIN_SOUND;
999
+ exports.PreCallLobby = PreCallLobby;
1000
+ exports.RING_SOUND = RING_SOUND;
1001
+ exports.playSound = playSound;
1002
+ exports.useConference = useConference;
1003
+ exports.useDevices = useDevices;
1004
+ exports.useIncomingCalls = useIncomingCalls;