vg-x07df 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.
Files changed (140) hide show
  1. package/.azure-pipelines/publish-public.yml +37 -0
  2. package/.azure-pipelines/publish.yml +39 -0
  3. package/.changeset/README.md +8 -0
  4. package/.changeset/config.json +11 -0
  5. package/AUTO_JOIN_GUIDE.md +411 -0
  6. package/README.md +215 -0
  7. package/Screenshot 2025-09-24 at 14.34.48.png +0 -0
  8. package/Screenshot 2025-10-04 at 12.58.54.png +0 -0
  9. package/biome.json +48 -0
  10. package/examples/demo/.env.example +19 -0
  11. package/examples/demo/CHANGELOG.md +22 -0
  12. package/examples/demo/README.md +72 -0
  13. package/examples/demo/eslint.config.js +23 -0
  14. package/examples/demo/index.html +13 -0
  15. package/examples/demo/package.json +34 -0
  16. package/examples/demo/pnpm-lock.yaml +2098 -0
  17. package/examples/demo/pnpm-workspace.yaml +1 -0
  18. package/examples/demo/public/vite.svg +1 -0
  19. package/examples/demo/src/App.css +52 -0
  20. package/examples/demo/src/App.tsx +176 -0
  21. package/examples/demo/src/assets/react.svg +1 -0
  22. package/examples/demo/src/components/auth/LoginForm.css +144 -0
  23. package/examples/demo/src/components/auth/LoginForm.tsx +80 -0
  24. package/examples/demo/src/components/calling/AutoJoinSettings.tsx +213 -0
  25. package/examples/demo/src/components/calling/AutoJoinStatus.tsx +72 -0
  26. package/examples/demo/src/components/calling/CallInitiator.css +258 -0
  27. package/examples/demo/src/components/calling/CallInitiator.tsx +142 -0
  28. package/examples/demo/src/components/calling/CallNotifications.css +119 -0
  29. package/examples/demo/src/components/calling/CallNotifications.tsx +108 -0
  30. package/examples/demo/src/components/calling/IncomingCallModal.css +192 -0
  31. package/examples/demo/src/components/calling/IncomingCallModal.tsx +78 -0
  32. package/examples/demo/src/components/calling/MinimizedCall.css +156 -0
  33. package/examples/demo/src/components/calling/MinimizedCall.tsx +78 -0
  34. package/examples/demo/src/components/conference/ConferenceHeader.css +265 -0
  35. package/examples/demo/src/components/conference/ConferenceHeader.tsx +78 -0
  36. package/examples/demo/src/components/conference/EnhancedControlBar.css +356 -0
  37. package/examples/demo/src/components/conference/EnhancedControlBar.tsx +262 -0
  38. package/examples/demo/src/components/conference/PaginationControls.css +67 -0
  39. package/examples/demo/src/components/conference/PaginationControls.tsx +64 -0
  40. package/examples/demo/src/components/conference/ParticipantGrid.css +153 -0
  41. package/examples/demo/src/components/conference/ParticipantGrid.tsx +87 -0
  42. package/examples/demo/src/components/conference/ParticipantTile.css +210 -0
  43. package/examples/demo/src/components/conference/ParticipantTile.tsx +114 -0
  44. package/examples/demo/src/components/conference/VideoConference.css +214 -0
  45. package/examples/demo/src/components/conference/VideoConference.tsx +93 -0
  46. package/examples/demo/src/contexts/AuthContext.tsx +105 -0
  47. package/examples/demo/src/hooks/useAuth.ts +5 -0
  48. package/examples/demo/src/hooks/useCallTimer.ts +42 -0
  49. package/examples/demo/src/index.css +68 -0
  50. package/examples/demo/src/main.tsx +10 -0
  51. package/examples/demo/src/services/auth.service.ts +153 -0
  52. package/examples/demo/src/types/auth.types.ts +31 -0
  53. package/examples/demo/tsconfig.app.json +28 -0
  54. package/examples/demo/tsconfig.json +7 -0
  55. package/examples/demo/tsconfig.node.json +26 -0
  56. package/examples/demo/vite.config.ts +15 -0
  57. package/images/callpad-without-ai.png +0 -0
  58. package/package.json +28 -0
  59. package/packages/sdk/CHANGELOG.md +33 -0
  60. package/packages/sdk/LICENSE +21 -0
  61. package/packages/sdk/README.md +97 -0
  62. package/packages/sdk/documentation.md +1132 -0
  63. package/packages/sdk/openapi-ts.config.ts +7 -0
  64. package/packages/sdk/package.json +88 -0
  65. package/packages/sdk/src/core/auth.manager.ts +52 -0
  66. package/packages/sdk/src/core/events/event-bus.ts +301 -0
  67. package/packages/sdk/src/core/events/index.ts +8 -0
  68. package/packages/sdk/src/core/events/types.ts +165 -0
  69. package/packages/sdk/src/core/index.ts +3 -0
  70. package/packages/sdk/src/core/signal/api.config.ts +49 -0
  71. package/packages/sdk/src/core/signal/index.ts +16 -0
  72. package/packages/sdk/src/core/signal/signal.client.ts +101 -0
  73. package/packages/sdk/src/core/signal/types.ts +110 -0
  74. package/packages/sdk/src/core/socketio/handlers/base.handler.ts +212 -0
  75. package/packages/sdk/src/core/socketio/handlers/call-accepted.handler.ts +34 -0
  76. package/packages/sdk/src/core/socketio/handlers/call-canceled.handler.ts +34 -0
  77. package/packages/sdk/src/core/socketio/handlers/call-declined.handler.ts +29 -0
  78. package/packages/sdk/src/core/socketio/handlers/call-ended.handler.ts +40 -0
  79. package/packages/sdk/src/core/socketio/handlers/call-incoming.handler.ts +72 -0
  80. package/packages/sdk/src/core/socketio/handlers/call-join-info.handler.ts +181 -0
  81. package/packages/sdk/src/core/socketio/handlers/call-participant-joined.handler.ts +42 -0
  82. package/packages/sdk/src/core/socketio/handlers/call-participant-joining.handler.ts +42 -0
  83. package/packages/sdk/src/core/socketio/handlers/call-timeout.handler.ts +31 -0
  84. package/packages/sdk/src/core/socketio/handlers/handler.registry.ts +62 -0
  85. package/packages/sdk/src/core/socketio/handlers/index.ts +21 -0
  86. package/packages/sdk/src/core/socketio/handlers/participant-left.handler.ts +37 -0
  87. package/packages/sdk/src/core/socketio/handlers/schema.ts +130 -0
  88. package/packages/sdk/src/core/socketio/index.ts +5 -0
  89. package/packages/sdk/src/core/socketio/socket.manager.ts +187 -0
  90. package/packages/sdk/src/core/socketio/types.ts +14 -0
  91. package/packages/sdk/src/core/types.ts +23 -0
  92. package/packages/sdk/src/generated/api/core/ApiError.ts +21 -0
  93. package/packages/sdk/src/generated/api/core/ApiRequestOptions.ts +13 -0
  94. package/packages/sdk/src/generated/api/core/ApiResult.ts +7 -0
  95. package/packages/sdk/src/generated/api/core/CancelablePromise.ts +126 -0
  96. package/packages/sdk/src/generated/api/core/OpenAPI.ts +55 -0
  97. package/packages/sdk/src/generated/api/core/request.ts +339 -0
  98. package/packages/sdk/src/generated/api/index.ts +5 -0
  99. package/packages/sdk/src/generated/api/models.ts +219 -0
  100. package/packages/sdk/src/generated/api/services.ts +225 -0
  101. package/packages/sdk/src/hooks/index.ts +21 -0
  102. package/packages/sdk/src/hooks/useAutoJoin.ts +66 -0
  103. package/packages/sdk/src/hooks/useCallActions.ts +28 -0
  104. package/packages/sdk/src/hooks/useCallQuality.ts +416 -0
  105. package/packages/sdk/src/hooks/useCallState.ts +23 -0
  106. package/packages/sdk/src/hooks/useConnection.ts +15 -0
  107. package/packages/sdk/src/hooks/useDevices.ts +296 -0
  108. package/packages/sdk/src/hooks/useErrorRecovery.ts +299 -0
  109. package/packages/sdk/src/hooks/useErrors.ts +84 -0
  110. package/packages/sdk/src/hooks/useEvent.ts +188 -0
  111. package/packages/sdk/src/hooks/useMediaControls.ts +215 -0
  112. package/packages/sdk/src/hooks/useParticipantStatus.ts +318 -0
  113. package/packages/sdk/src/hooks/useParticipants.ts +111 -0
  114. package/packages/sdk/src/index.ts +66 -0
  115. package/packages/sdk/src/livekit/constants.ts +76 -0
  116. package/packages/sdk/src/livekit/device.manager.ts +172 -0
  117. package/packages/sdk/src/livekit/error-classifier.ts +155 -0
  118. package/packages/sdk/src/livekit/events/eventBridge.ts +371 -0
  119. package/packages/sdk/src/livekit/events/trackRegistry.ts +114 -0
  120. package/packages/sdk/src/livekit/index.ts +49 -0
  121. package/packages/sdk/src/livekit/livekit.service.ts +110 -0
  122. package/packages/sdk/src/livekit/media.controls.ts +315 -0
  123. package/packages/sdk/src/livekit/room.manager.ts +79 -0
  124. package/packages/sdk/src/livekit/track.utils.ts +230 -0
  125. package/packages/sdk/src/livekit/types.ts +135 -0
  126. package/packages/sdk/src/provider/RtcProvider.tsx +78 -0
  127. package/packages/sdk/src/services/call-actions.ts +260 -0
  128. package/packages/sdk/src/services/error-recovery.ts +461 -0
  129. package/packages/sdk/src/services/index.ts +2 -0
  130. package/packages/sdk/src/services/sdk-builder.ts +104 -0
  131. package/packages/sdk/src/state/errors.ts +163 -0
  132. package/packages/sdk/src/state/selectors.ts +28 -0
  133. package/packages/sdk/src/state/store.ts +36 -0
  134. package/packages/sdk/src/state/types.ts +151 -0
  135. package/packages/sdk/src/utils/logger.ts +183 -0
  136. package/packages/sdk/tsconfig.json +49 -0
  137. package/packages/sdk/tsup.config.ts +51 -0
  138. package/pnpm-workspace.yaml +4 -0
  139. package/tsconfig.base.json +19 -0
  140. package/turbo.json +34 -0
@@ -0,0 +1,188 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { eventBus } from "../core/events";
3
+ import { SdkEventType } from "../core/events/types";
4
+ import type { EventFilter, EventHandler, SdkEvent } from "../core/events/types";
5
+
6
+ /**
7
+ * Hook for subscribing to SDK events
8
+ *
9
+ * @example
10
+ * // Listen for specific event type
11
+ * const callEvent = useEvent(SdkEventType.CALL_ACCEPTED);
12
+ *
13
+ * @example
14
+ * // Listen with callback
15
+ * useEvent(SdkEventType.MEDIA_ENABLED, (event) => {
16
+ * // Handle media enabled event
17
+ * });
18
+ *
19
+ * @example
20
+ * // Listen to pattern with filter
21
+ * useEvent('call:*', null, (event) => event.payload.callId === 'specific-call');
22
+ */
23
+ export function useEvent<T = any>(
24
+ eventType: string | SdkEventType,
25
+ callback?: EventHandler<T> | null,
26
+ filter?: EventFilter<T>
27
+ ): SdkEvent<T> | undefined {
28
+ const [lastEvent, setLastEvent] = useState<SdkEvent<T> | undefined>(
29
+ undefined
30
+ );
31
+ const callbackRef = useRef(callback);
32
+ callbackRef.current = callback;
33
+
34
+ useEffect(() => {
35
+ const handler: EventHandler<T> = (event: SdkEvent<T>) => {
36
+ setLastEvent(event);
37
+ if (callbackRef.current) {
38
+ callbackRef.current(event);
39
+ }
40
+ };
41
+
42
+ // Support pattern matching (e.g., "call:*")
43
+ const subscription = eventType.includes("*")
44
+ ? eventBus.onPattern(eventType, handler, filter)
45
+ : eventBus.on(eventType, handler, filter);
46
+
47
+ return () => {
48
+ subscription.unsubscribe();
49
+ };
50
+ }, [eventType, filter]);
51
+
52
+ return lastEvent;
53
+ }
54
+
55
+ /**
56
+ * Hook for subscribing to events once
57
+ *
58
+ * @example
59
+ * useEventOnce(SdkEventType.CALL_ACCEPTED, (event) => {
60
+ * // Handle call accepted event once
61
+ * });
62
+ */
63
+ export function useEventOnce<T = any>(
64
+ eventType: string | SdkEventType,
65
+ callback: EventHandler<T>,
66
+ filter?: EventFilter<T>
67
+ ): void {
68
+ const callbackRef = useRef(callback);
69
+ callbackRef.current = callback;
70
+
71
+ useEffect(() => {
72
+ const subscription = eventBus.once(eventType, callbackRef.current, filter);
73
+
74
+ return () => {
75
+ subscription.unsubscribe();
76
+ };
77
+ }, [eventType, filter]);
78
+ }
79
+
80
+ /**
81
+ * Hook for accessing the event bus directly
82
+ * Use this for advanced event management scenarios
83
+ *
84
+ * @example
85
+ * const events = useEventBus();
86
+ *
87
+ * // Emit custom event
88
+ * events.emit('custom:event', { data: 'test' });
89
+ *
90
+ * // Get event history
91
+ * const history = events.getEventHistory();
92
+ */
93
+ export function useEventBus() {
94
+ return eventBus;
95
+ }
96
+
97
+ /**
98
+ * Hook for getting events matching a condition
99
+ *
100
+ * @example
101
+ * const callEvents = useEventHistory((event) =>
102
+ * event.type.startsWith('call:') &&
103
+ * event.payload.callId === currentCallId
104
+ * );
105
+ */
106
+ export function useEventHistory<T = any>(
107
+ filter?: EventFilter<T>
108
+ ): SdkEvent<T>[] {
109
+ const [events, setEvents] = useState<SdkEvent<T>[]>(() =>
110
+ filter ? eventBus.getEventsWhere(filter) : eventBus.getEventHistory()
111
+ );
112
+
113
+ useEffect(() => {
114
+ const updateEvents = () => {
115
+ const newEvents = filter
116
+ ? eventBus.getEventsWhere(filter)
117
+ : eventBus.getEventHistory();
118
+ setEvents(newEvents);
119
+ };
120
+
121
+ // Subscribe to any event to trigger updates
122
+ const subscription = eventBus.onPattern("*", updateEvents);
123
+
124
+ return () => {
125
+ subscription.unsubscribe();
126
+ };
127
+ }, [filter]);
128
+
129
+ return events;
130
+ }
131
+
132
+ /**
133
+ * Hook for consuming call-specific events
134
+ * Automatically filters events by call ID
135
+ *
136
+ * @example
137
+ * const { callAccepted, callDeclined, participantJoined } = useCallEvents(callId);
138
+ */
139
+ export function useCallEvents(callId?: string) {
140
+ const callFilter: EventFilter = (event) =>
141
+ !callId || event.payload?.callId === callId;
142
+
143
+ const callAccepted = useEvent(SdkEventType.CALL_ACCEPTED, null, callFilter);
144
+ const callDeclined = useEvent(SdkEventType.CALL_DECLINED, null, callFilter);
145
+ const callEnded = useEvent(SdkEventType.CALL_ENDED, null, callFilter);
146
+ const participantJoined = useEvent(
147
+ SdkEventType.PARTICIPANT_JOINED,
148
+ null,
149
+ callFilter
150
+ );
151
+ const participantLeft = useEvent(
152
+ SdkEventType.PARTICIPANT_LEFT,
153
+ null,
154
+ callFilter
155
+ );
156
+
157
+ return {
158
+ callAccepted,
159
+ callDeclined,
160
+ callEnded,
161
+ participantJoined,
162
+ participantLeft,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Hook for consuming media events
168
+ * Automatically filters events by participant ID
169
+ *
170
+ * @example
171
+ * const { mediaEnabled, mediaDisabled } = useMediaEvents(participantId);
172
+ */
173
+ export function useMediaEvents(participantId?: string) {
174
+ const mediaFilter: EventFilter = (event) =>
175
+ !participantId || event.payload?.participantId === participantId;
176
+
177
+ const mediaEnabled = useEvent(SdkEventType.MEDIA_ENABLED, null, mediaFilter);
178
+ const mediaDisabled = useEvent(
179
+ SdkEventType.MEDIA_DISABLED,
180
+ null,
181
+ mediaFilter
182
+ );
183
+
184
+ return {
185
+ mediaEnabled,
186
+ mediaDisabled,
187
+ };
188
+ }
@@ -0,0 +1,215 @@
1
+ import { useState } from "react";
2
+ import type { MediaActions } from "../livekit";
3
+ import { useSdk } from "../provider/RtcProvider";
4
+ import { useRtcStore } from "../state/store";
5
+ import type { RtcError } from "../state/types";
6
+ import { createLogger } from "../utils/logger";
7
+ import { useDevices } from "./useDevices";
8
+
9
+ const logger = createLogger("hooks:media-controls");
10
+
11
+ export interface MediaControlsState {
12
+ isVideoEnabled: boolean;
13
+ isAudioEnabled: boolean;
14
+ isCameraAvailable: boolean;
15
+ isMicrophoneAvailable: boolean;
16
+ isConnected: boolean;
17
+ isLoading: boolean;
18
+ errors: RtcError[];
19
+ }
20
+
21
+ export interface EnhancedMediaActions {
22
+ enableCamera: () => Promise<void>;
23
+ disableCamera: () => Promise<void>;
24
+ enableMicrophone: () => Promise<void>;
25
+ disableMicrophone: () => Promise<void>;
26
+ toggleCamera: () => Promise<void>;
27
+ toggleMicrophone: () => Promise<void>;
28
+ // Device switching
29
+ switchCamera: (deviceId: string) => Promise<void>;
30
+ switchMicrophone: (deviceId: string) => Promise<void>;
31
+ // Simple aliases
32
+ toggleAudio: () => Promise<void>;
33
+ toggleVideo: () => Promise<void>;
34
+ }
35
+
36
+ export interface MediaControlsHook
37
+ extends MediaControlsState,
38
+ EnhancedMediaActions {}
39
+
40
+ export function useMediaControls(): MediaControlsHook & {
41
+ devices: {
42
+ cameras: any[];
43
+ microphones: any[];
44
+ speakers: any[];
45
+ };
46
+ } {
47
+ const sdk = useSdk();
48
+ const local = useRtcStore((state) => state.local);
49
+ const connection = useRtcStore((state) => state.connection);
50
+ const errors = useRtcStore((state) =>
51
+ state.errors.filter(
52
+ (e) =>
53
+ e.code.startsWith("CAMERA_") ||
54
+ e.code.startsWith("MICROPHONE_") ||
55
+ e.code.startsWith("LIVEKIT_")
56
+ )
57
+ );
58
+ const devices = useDevices();
59
+
60
+ const [isLoading, setIsLoading] = useState(false);
61
+
62
+ // Check if media controls are available and connected
63
+ const isConnected = connection.connected;
64
+ let mediaControls: MediaActions | null = null;
65
+
66
+ try {
67
+ if (sdk.livekit && isConnected) {
68
+ mediaControls = sdk.livekit.media;
69
+ }
70
+ } catch {
71
+ // Media controls not available - room not connected
72
+ mediaControls = null;
73
+ }
74
+
75
+ // Enhanced wrapper functions with loading states and better error handling
76
+ const createEnhancedAction = (
77
+ action: () => Promise<void>,
78
+ actionName: string
79
+ ) => {
80
+ return async (): Promise<void> => {
81
+ if (!mediaControls) {
82
+ const errorMsg = !isConnected
83
+ ? "Cannot control media - not connected to LiveKit room"
84
+ : "Media controls not available - LiveKit service not initialized";
85
+ throw new Error(errorMsg);
86
+ }
87
+
88
+ setIsLoading(true);
89
+ try {
90
+ await action();
91
+ } catch (error) {
92
+ // Enhanced error handling with context
93
+ logger.error(`Failed to ${actionName}`, { actionName, error });
94
+ throw error;
95
+ } finally {
96
+ setIsLoading(false);
97
+ }
98
+ };
99
+ };
100
+
101
+ // Fallback functions for when media controls are not available
102
+ const unavailableAction = async (): Promise<void> => {
103
+ const errorMsg = !isConnected
104
+ ? "Cannot control media - not connected to LiveKit room"
105
+ : "Media controls not available - LiveKit service not initialized";
106
+ throw new Error(errorMsg);
107
+ };
108
+
109
+ // Simple device switching functions without complex error handling
110
+ const switchCamera = async (deviceId: string): Promise<void> => {
111
+ try {
112
+ if (sdk.livekit?.devices) {
113
+ await sdk.livekit.devices.switchCamera(deviceId);
114
+ }
115
+ } catch (error) {
116
+ logger.error("Failed to switch camera", { error, deviceId });
117
+ throw error;
118
+ }
119
+ };
120
+
121
+ const switchMicrophone = async (deviceId: string): Promise<void> => {
122
+ try {
123
+ if (sdk.livekit?.devices) {
124
+ await sdk.livekit.devices.switchMicrophone(deviceId);
125
+ }
126
+ } catch (error) {
127
+ logger.error("Failed to switch microphone", { error, deviceId });
128
+ throw error;
129
+ }
130
+ };
131
+
132
+ const actions: EnhancedMediaActions = mediaControls
133
+ ? {
134
+ enableCamera: createEnhancedAction(
135
+ () => mediaControls?.enableCamera(),
136
+ "enable camera"
137
+ ),
138
+ disableCamera: createEnhancedAction(
139
+ () => mediaControls?.disableCamera(),
140
+ "disable camera"
141
+ ),
142
+ enableMicrophone: createEnhancedAction(
143
+ () => mediaControls?.enableMicrophone(),
144
+ "enable microphone"
145
+ ),
146
+ disableMicrophone: createEnhancedAction(
147
+ () => mediaControls?.disableMicrophone(),
148
+ "disable microphone"
149
+ ),
150
+ toggleCamera: createEnhancedAction(
151
+ () => mediaControls?.toggleCamera(),
152
+ "toggle camera"
153
+ ),
154
+ toggleMicrophone: createEnhancedAction(
155
+ () => mediaControls?.toggleMicrophone(),
156
+ "toggle microphone"
157
+ ),
158
+ // Device switching
159
+ switchCamera,
160
+ switchMicrophone,
161
+ // Simple aliases
162
+ toggleAudio: createEnhancedAction(
163
+ () => mediaControls?.toggleMicrophone(),
164
+ "toggle audio"
165
+ ),
166
+ toggleVideo: createEnhancedAction(
167
+ () => mediaControls?.toggleCamera(),
168
+ "toggle video"
169
+ ),
170
+ }
171
+ : {
172
+ enableCamera: unavailableAction,
173
+ disableCamera: unavailableAction,
174
+ enableMicrophone: unavailableAction,
175
+ disableMicrophone: unavailableAction,
176
+ toggleCamera: unavailableAction,
177
+ toggleMicrophone: unavailableAction,
178
+ switchCamera: unavailableAction,
179
+ switchMicrophone: unavailableAction,
180
+ toggleAudio: unavailableAction,
181
+ toggleVideo: unavailableAction,
182
+ };
183
+
184
+ return {
185
+ // State
186
+ isVideoEnabled: local.videoEnabled,
187
+ isAudioEnabled: local.audioEnabled,
188
+ isCameraAvailable: !!mediaControls,
189
+ isMicrophoneAvailable: !!mediaControls,
190
+ isConnected,
191
+ isLoading,
192
+ errors,
193
+
194
+ // Device access
195
+ devices: {
196
+ cameras: devices.cams,
197
+ microphones: devices.mics,
198
+ speakers: devices.speakers,
199
+ },
200
+
201
+ // Enhanced Actions
202
+ enableCamera: actions.enableCamera,
203
+ disableCamera: actions.disableCamera,
204
+ enableMicrophone: actions.enableMicrophone,
205
+ disableMicrophone: actions.disableMicrophone,
206
+ toggleCamera: actions.toggleCamera,
207
+ toggleMicrophone: actions.toggleMicrophone,
208
+ // Device switching
209
+ switchCamera: actions.switchCamera,
210
+ switchMicrophone: actions.switchMicrophone,
211
+ // Simple aliases
212
+ toggleAudio: actions.toggleAudio,
213
+ toggleVideo: actions.toggleVideo,
214
+ };
215
+ }
@@ -0,0 +1,318 @@
1
+ import { useEffect, useState } from "react";
2
+ import { SdkEventType, eventBus } from "../core/events";
3
+ import { useRtcStore } from "../state/store";
4
+ import type { Participant } from "../state/types";
5
+
6
+ /**
7
+ * Enhanced participant status interface following spec requirements
8
+ */
9
+ export interface ParticipantStatus {
10
+ connectionState: "connecting" | "connected" | "reconnecting" | "disconnected";
11
+ mediaState: {
12
+ audio: "enabled" | "disabled" | "muted";
13
+ video: "enabled" | "disabled" | "camera_off";
14
+ };
15
+ networkQuality: "excellent" | "good" | "poor" | "lost" | "unknown";
16
+ lastSeen?: number;
17
+ speaking?: boolean;
18
+ }
19
+
20
+ /**
21
+ * Hook for tracking real-time participant status
22
+ */
23
+ export function useParticipantStatus(participantId: string): ParticipantStatus {
24
+ const participant = useRtcStore(
25
+ (state) => state.room.participants[participantId]
26
+ );
27
+ const connection = useRtcStore((state) => state.connection);
28
+
29
+ const [status, setStatus] = useState<ParticipantStatus>(() =>
30
+ getInitialStatus(participant || null, connection.connected)
31
+ );
32
+
33
+ // Update status when participant data changes
34
+ useEffect(() => {
35
+ if (!participant) {
36
+ setStatus(getDisconnectedStatus());
37
+ return;
38
+ }
39
+
40
+ setStatus((prevStatus) => ({
41
+ ...prevStatus,
42
+ connectionState: getConnectionState(participant, connection.connected),
43
+ mediaState: {
44
+ audio: getAudioState(participant),
45
+ video: getVideoState(participant),
46
+ },
47
+ networkQuality: participant.connectionQuality || "unknown",
48
+ lastSeen: participant.joinedAt || Date.now(),
49
+ speaking: participant.isSpeaking || false,
50
+ }));
51
+ }, [participant, connection.connected]);
52
+
53
+ // Listen for real-time media events
54
+ useEffect(() => {
55
+ const mediaEnabledSub = eventBus.on(SdkEventType.MEDIA_ENABLED, (event) => {
56
+ if (event.payload.participantId === participantId) {
57
+ setStatus((prevStatus) => ({
58
+ ...prevStatus,
59
+ mediaState: {
60
+ ...prevStatus.mediaState,
61
+ [event.payload.mediaType]: "enabled",
62
+ },
63
+ }));
64
+ }
65
+ });
66
+
67
+ const mediaDisabledSub = eventBus.on(
68
+ SdkEventType.MEDIA_DISABLED,
69
+ (event) => {
70
+ if (event.payload.participantId === participantId) {
71
+ setStatus((prevStatus) => ({
72
+ ...prevStatus,
73
+ mediaState: {
74
+ ...prevStatus.mediaState,
75
+ [event.payload.mediaType]: "disabled",
76
+ },
77
+ }));
78
+ }
79
+ }
80
+ );
81
+
82
+ const connectionQualitySub = eventBus.on(
83
+ SdkEventType.CONNECTION_QUALITY_CHANGED,
84
+ (event) => {
85
+ if (event.payload.participantId === participantId) {
86
+ setStatus((prevStatus) => ({
87
+ ...prevStatus,
88
+ networkQuality: event.payload.quality,
89
+ }));
90
+ }
91
+ }
92
+ );
93
+
94
+ const participantJoinedSub = eventBus.on(
95
+ SdkEventType.PARTICIPANT_JOINED,
96
+ (event) => {
97
+ if (event.payload.participant.id === participantId) {
98
+ setStatus((prevStatus) => ({
99
+ ...prevStatus,
100
+ connectionState: "connected",
101
+ lastSeen: event.timestamp,
102
+ }));
103
+ }
104
+ }
105
+ );
106
+
107
+ const participantLeftSub = eventBus.on(
108
+ SdkEventType.PARTICIPANT_LEFT,
109
+ (event) => {
110
+ if (event.payload.participantId === participantId) {
111
+ setStatus((prevStatus) => ({
112
+ ...prevStatus,
113
+ connectionState: "disconnected",
114
+ lastSeen: event.timestamp,
115
+ }));
116
+ }
117
+ }
118
+ );
119
+
120
+ return () => {
121
+ mediaEnabledSub.unsubscribe();
122
+ mediaDisabledSub.unsubscribe();
123
+ connectionQualitySub.unsubscribe();
124
+ participantJoinedSub.unsubscribe();
125
+ participantLeftSub.unsubscribe();
126
+ };
127
+ }, [participantId]);
128
+
129
+ return status;
130
+ }
131
+
132
+ /**
133
+ * Hook for tracking multiple participants' status
134
+ */
135
+ export function useParticipantsStatus(
136
+ participantIds: string[]
137
+ ): Record<string, ParticipantStatus> {
138
+ const [statuses, setStatuses] = useState<Record<string, ParticipantStatus>>(
139
+ {}
140
+ );
141
+
142
+ useEffect(() => {
143
+ const updateStatus = (id: string, status: ParticipantStatus) => {
144
+ setStatuses((prev) => ({
145
+ ...prev,
146
+ [id]: status,
147
+ }));
148
+ };
149
+
150
+ // Initialize statuses
151
+ const initialStatuses: Record<string, ParticipantStatus> = {};
152
+ for (const id of participantIds) {
153
+ initialStatuses[id] = getInitialStatus(null, false);
154
+ }
155
+ setStatuses(initialStatuses);
156
+
157
+ // Set up event listeners for all participants
158
+ const subscriptions = [
159
+ eventBus.onPattern("*", (event) => {
160
+ const participantId =
161
+ event.payload?.participantId || event.payload?.participant?.id;
162
+ if (participantId && participantIds.includes(participantId)) {
163
+ // Update the specific participant's status
164
+ setStatuses((prev) => {
165
+ const currentStatus =
166
+ prev[participantId] || getInitialStatus(null, false);
167
+ return {
168
+ ...prev,
169
+ [participantId]: updateStatusFromEvent(currentStatus, event),
170
+ };
171
+ });
172
+ }
173
+ }),
174
+ ];
175
+
176
+ return () => {
177
+ for (const sub of subscriptions) {
178
+ sub.unsubscribe();
179
+ }
180
+ };
181
+ }, [participantIds]);
182
+
183
+ return statuses;
184
+ }
185
+
186
+ /**
187
+ * Hook for getting all participants with their real-time status
188
+ */
189
+ export function useParticipantsWithStatus(): (Participant & {
190
+ status: ParticipantStatus;
191
+ })[] {
192
+ const participants = useRtcStore((state) =>
193
+ Object.values(state.room.participants)
194
+ );
195
+ const connection = useRtcStore((state) => state.connection);
196
+
197
+ return participants.map((participant) => ({
198
+ ...participant,
199
+ status: {
200
+ connectionState: getConnectionState(participant, connection.connected),
201
+ mediaState: {
202
+ audio: getAudioState(participant),
203
+ video: getVideoState(participant),
204
+ },
205
+ networkQuality: participant.connectionQuality || "unknown",
206
+ lastSeen: participant.joinedAt || Date.now(),
207
+ speaking: participant.isSpeaking || false,
208
+ },
209
+ }));
210
+ }
211
+
212
+ function getInitialStatus(
213
+ participant: Participant | null,
214
+ isConnected: boolean
215
+ ): ParticipantStatus {
216
+ if (!participant) {
217
+ return getDisconnectedStatus();
218
+ }
219
+
220
+ return {
221
+ connectionState: getConnectionState(participant, isConnected),
222
+ mediaState: {
223
+ audio: getAudioState(participant),
224
+ video: getVideoState(participant),
225
+ },
226
+ networkQuality: participant.connectionQuality || "unknown",
227
+ lastSeen: participant.joinedAt || Date.now(),
228
+ speaking: participant.isSpeaking || false,
229
+ };
230
+ }
231
+
232
+ function getDisconnectedStatus(): ParticipantStatus {
233
+ return {
234
+ connectionState: "disconnected",
235
+ mediaState: {
236
+ audio: "disabled",
237
+ video: "disabled",
238
+ },
239
+ networkQuality: "unknown",
240
+ speaking: false,
241
+ };
242
+ }
243
+
244
+ function getConnectionState(
245
+ participant: Participant,
246
+ isGloballyConnected: boolean
247
+ ): ParticipantStatus["connectionState"] {
248
+ if (!isGloballyConnected) {
249
+ return "disconnected";
250
+ }
251
+
252
+ switch (participant.callState) {
253
+ case "JOINED":
254
+ return "connected";
255
+ case "RINGING":
256
+ case "INVITED":
257
+ return "connecting";
258
+ case "LEFT":
259
+ return "disconnected";
260
+ default:
261
+ return "disconnected";
262
+ }
263
+ }
264
+
265
+ function getAudioState(
266
+ participant: Participant
267
+ ): ParticipantStatus["mediaState"]["audio"] {
268
+ if (!participant.audioEnabled) {
269
+ return "disabled";
270
+ }
271
+ // Could add muted state detection here based on additional data
272
+ return "enabled";
273
+ }
274
+
275
+ function getVideoState(
276
+ participant: Participant
277
+ ): ParticipantStatus["mediaState"]["video"] {
278
+ if (!participant.videoEnabled) {
279
+ return "camera_off";
280
+ }
281
+ return "enabled";
282
+ }
283
+
284
+ function updateStatusFromEvent(
285
+ currentStatus: ParticipantStatus,
286
+ event: any
287
+ ): ParticipantStatus {
288
+ const newStatus = { ...currentStatus };
289
+
290
+ switch (event.type) {
291
+ case SdkEventType.MEDIA_ENABLED:
292
+ newStatus.mediaState = {
293
+ ...newStatus.mediaState,
294
+ [event.payload.mediaType]: "enabled",
295
+ };
296
+ break;
297
+ case SdkEventType.MEDIA_DISABLED:
298
+ newStatus.mediaState = {
299
+ ...newStatus.mediaState,
300
+ [event.payload.mediaType]:
301
+ event.payload.mediaType === "video" ? "camera_off" : "disabled",
302
+ };
303
+ break;
304
+ case SdkEventType.CONNECTION_QUALITY_CHANGED:
305
+ newStatus.networkQuality = event.payload.quality;
306
+ break;
307
+ case SdkEventType.PARTICIPANT_JOINED:
308
+ newStatus.connectionState = "connected";
309
+ newStatus.lastSeen = event.timestamp;
310
+ break;
311
+ case SdkEventType.PARTICIPANT_LEFT:
312
+ newStatus.connectionState = "disconnected";
313
+ newStatus.lastSeen = event.timestamp;
314
+ break;
315
+ }
316
+
317
+ return newStatus;
318
+ }