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