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,296 @@
1
+ import { Room } from "livekit-client";
2
+ import { useCallback, useEffect, useState } from "react";
3
+ import { useSdk } from "../provider/RtcProvider";
4
+ import { useRtcStore } from "../state/store";
5
+ import type { DeviceState, PermissionStatus, RtcError } from "../state/types";
6
+ import { createLogger } from "../utils/logger";
7
+
8
+ const logger = createLogger("hooks:devices");
9
+
10
+ export interface DeviceActions {
11
+ switchCamera: (deviceId: string) => Promise<void>;
12
+ switchMicrophone: (deviceId: string) => Promise<void>;
13
+ switchSpeaker: (deviceId: string) => Promise<void>;
14
+ listDevices: () => Promise<void>;
15
+ refreshDevices: () => Promise<void>;
16
+ requestPermissions: (kind: "microphone" | "camera" | "both") => Promise<void>;
17
+ checkPermissions: () => Promise<void>;
18
+ }
19
+
20
+ export interface DevicesHook extends DeviceState, DeviceActions {
21
+ isConnected: boolean;
22
+ errors: RtcError[];
23
+ }
24
+
25
+ export function useDevices(): DevicesHook {
26
+ const sdk = useSdk();
27
+ const devices = useRtcStore((state) => state.devices);
28
+ const connection = useRtcStore((state) => state.connection);
29
+ const errors = useRtcStore((state) =>
30
+ state.errors.filter((e) => e.code.startsWith("DEVICE_"))
31
+ );
32
+
33
+ const [localLoading, setLocalLoading] = useState(false);
34
+
35
+ const isConnected = connection.connected;
36
+ let deviceManager = null;
37
+
38
+ try {
39
+ if (sdk.livekit && isConnected) {
40
+ deviceManager = sdk.livekit.devices;
41
+ }
42
+ } catch {
43
+ deviceManager = null;
44
+ }
45
+
46
+ const createSwitchAction = useCallback(
47
+ (
48
+ switchFn: (deviceId: string) => Promise<void> | undefined,
49
+ actionName: string
50
+ ) => {
51
+ return async (deviceId: string): Promise<void> => {
52
+ if (!deviceManager) {
53
+ const errorMsg = !isConnected
54
+ ? "Cannot switch device - not connected to LiveKit room"
55
+ : "Device manager not available - LiveKit service not initialized";
56
+ throw new Error(errorMsg);
57
+ }
58
+
59
+ setLocalLoading(true);
60
+ try {
61
+ const result = switchFn(deviceId);
62
+ if (result) {
63
+ await result;
64
+ }
65
+ } catch (error) {
66
+ logger.error(`Failed to ${actionName}`, { actionName, error });
67
+ throw error;
68
+ } finally {
69
+ setLocalLoading(false);
70
+ }
71
+ };
72
+ },
73
+ [deviceManager, isConnected]
74
+ );
75
+
76
+ const switchCamera = createSwitchAction(
77
+ (deviceId: string) => deviceManager?.switchCamera(deviceId),
78
+ "switch camera"
79
+ );
80
+
81
+ const switchMicrophone = createSwitchAction(
82
+ (deviceId: string) => deviceManager?.switchMicrophone(deviceId),
83
+ "switch microphone"
84
+ );
85
+
86
+ const switchSpeaker = createSwitchAction(
87
+ (deviceId: string) => deviceManager?.switchSpeaker(deviceId),
88
+ "switch speaker"
89
+ );
90
+
91
+ // Pre-connection device listing using LiveKit static method
92
+ const listDevices = useCallback(async (): Promise<void> => {
93
+ setLocalLoading(true);
94
+ try {
95
+ const [mics, cams, speakers] = await Promise.all([
96
+ Room.getLocalDevices("audioinput", false), // Don't request permissions
97
+ Room.getLocalDevices("videoinput", false),
98
+ Room.getLocalDevices("audiooutput", false),
99
+ ]);
100
+
101
+ useRtcStore.getState().patch((state) => {
102
+ state.devices.mics = mics;
103
+ state.devices.cams = cams;
104
+ state.devices.speakers = speakers;
105
+ state.devices.isEnumerating = false;
106
+ state.devices.lastEnumeratedAt = Date.now();
107
+ });
108
+ } catch (error) {
109
+ logger.error("Failed to list devices", { error });
110
+ throw error;
111
+ } finally {
112
+ setLocalLoading(false);
113
+ }
114
+ }, []);
115
+
116
+ // Request permissions and refresh device labels
117
+ const requestPermissions = useCallback(
118
+ async (kind: "microphone" | "camera" | "both"): Promise<void> => {
119
+ setLocalLoading(true);
120
+ try {
121
+ // Use LiveKit's permission-requesting device enumeration
122
+ if (kind === "microphone" || kind === "both") {
123
+ await Room.getLocalDevices("audioinput", true); // Request permissions
124
+ useRtcStore.getState().patch((state) => {
125
+ state.devices.permissions.microphone = "granted";
126
+ });
127
+ }
128
+
129
+ if (kind === "camera" || kind === "both") {
130
+ await Room.getLocalDevices("videoinput", true); // Request permissions
131
+ useRtcStore.getState().patch((state) => {
132
+ state.devices.permissions.camera = "granted";
133
+ });
134
+ }
135
+
136
+ // Refresh all devices to get updated labels
137
+ await listDevices();
138
+ } catch (error) {
139
+ logger.error("Failed to request permissions", { kind, error });
140
+
141
+ // Update permission state based on error type
142
+ useRtcStore.getState().patch((state) => {
143
+ if (kind === "microphone" || kind === "both") {
144
+ state.devices.permissions.microphone = "denied";
145
+ }
146
+ if (kind === "camera" || kind === "both") {
147
+ state.devices.permissions.camera = "denied";
148
+ }
149
+ });
150
+
151
+ throw error;
152
+ } finally {
153
+ setLocalLoading(false);
154
+ }
155
+ },
156
+ [listDevices]
157
+ );
158
+
159
+ const refreshDevices = useCallback(async (): Promise<void> => {
160
+ if (deviceManager && isConnected) {
161
+ // Use connected device manager when available
162
+ setLocalLoading(true);
163
+ try {
164
+ await deviceManager.enumerateDevices();
165
+ } catch (error) {
166
+ logger.error("Failed to refresh devices", { error });
167
+ throw error;
168
+ } finally {
169
+ setLocalLoading(false);
170
+ }
171
+ } else {
172
+ // Fall back to pre-connection listing
173
+ await listDevices();
174
+ }
175
+ }, [deviceManager, isConnected, listDevices]);
176
+
177
+ const checkPermissions = useCallback(async (): Promise<void> => {
178
+ if (!navigator.permissions) {
179
+ return;
180
+ }
181
+
182
+ try {
183
+ const [cameraPermission, microphonePermission] = await Promise.all([
184
+ navigator.permissions.query({ name: "camera" as PermissionName }),
185
+ navigator.permissions.query({ name: "microphone" as PermissionName }),
186
+ ]);
187
+
188
+ useRtcStore.getState().patch((state) => {
189
+ state.devices.permissions.camera =
190
+ cameraPermission.state as PermissionStatus;
191
+ state.devices.permissions.microphone =
192
+ microphonePermission.state as PermissionStatus;
193
+ });
194
+
195
+ cameraPermission.onchange = () => {
196
+ useRtcStore.getState().patch((state) => {
197
+ state.devices.permissions.camera =
198
+ cameraPermission.state as PermissionStatus;
199
+ });
200
+ };
201
+
202
+ microphonePermission.onchange = () => {
203
+ useRtcStore.getState().patch((state) => {
204
+ state.devices.permissions.microphone =
205
+ microphonePermission.state as PermissionStatus;
206
+ });
207
+ };
208
+ } catch (error) {
209
+ logger.warn("Failed to check device permissions", { error });
210
+ useRtcStore.getState().patch((state) => {
211
+ state.devices.permissions.camera = "unknown";
212
+ state.devices.permissions.microphone = "unknown";
213
+ });
214
+ }
215
+ }, []);
216
+
217
+ useEffect(() => {
218
+ // Auto-list devices on mount (works pre-connection)
219
+ listDevices().catch((error) => {
220
+ logger.warn("Failed to auto-list devices", { error });
221
+ });
222
+
223
+ checkPermissions().catch((error) => {
224
+ logger.warn("Failed to check permissions", { error });
225
+ });
226
+ }, [listDevices, checkPermissions]);
227
+
228
+ useEffect(() => {
229
+ // Re-enumerate when connected to get more accurate device info
230
+ if (isConnected && deviceManager) {
231
+ refreshDevices().catch((error) => {
232
+ logger.warn("Failed to refresh devices after connection", { error });
233
+ });
234
+ }
235
+ }, [isConnected, deviceManager, refreshDevices]);
236
+
237
+ const unavailableAction = async (): Promise<void> => {
238
+ const errorMsg = !isConnected
239
+ ? "Cannot perform device operation - not connected to LiveKit room"
240
+ : "Device manager not available - LiveKit service not initialized";
241
+ throw new Error(errorMsg);
242
+ };
243
+
244
+ const isEnumerating = devices.isEnumerating || localLoading;
245
+
246
+ return {
247
+ mics: devices.mics,
248
+ cams: devices.cams,
249
+ speakers: devices.speakers,
250
+ selected: devices.selected,
251
+ permissions: devices.permissions,
252
+ isEnumerating,
253
+ lastEnumeratedAt: devices.lastEnumeratedAt,
254
+
255
+ isConnected,
256
+ errors,
257
+
258
+ switchCamera: deviceManager ? switchCamera : unavailableAction,
259
+ switchMicrophone: deviceManager ? switchMicrophone : unavailableAction,
260
+ switchSpeaker: deviceManager ? switchSpeaker : unavailableAction,
261
+ listDevices, // Always available (works pre-connection)
262
+ refreshDevices, // Now works both pre and post connection
263
+ requestPermissions, // Always available (works pre-connection)
264
+ checkPermissions,
265
+ };
266
+ }
267
+
268
+ export function useDeviceState(): DeviceState {
269
+ return useRtcStore((state) => state.devices);
270
+ }
271
+
272
+ export function useDevicePermissions(): {
273
+ camera: PermissionStatus;
274
+ microphone: PermissionStatus;
275
+ isPermissionGranted: (type: "camera" | "microphone") => boolean;
276
+ hasAnyPermission: boolean;
277
+ } {
278
+ const permissions = useRtcStore((state) => state.devices.permissions);
279
+
280
+ const isPermissionGranted = useCallback(
281
+ (type: "camera" | "microphone"): boolean => {
282
+ return permissions[type] === "granted";
283
+ },
284
+ [permissions]
285
+ );
286
+
287
+ const hasAnyPermission =
288
+ permissions.camera === "granted" || permissions.microphone === "granted";
289
+
290
+ return {
291
+ camera: permissions.camera,
292
+ microphone: permissions.microphone,
293
+ isPermissionGranted,
294
+ hasAnyPermission,
295
+ };
296
+ }
@@ -0,0 +1,299 @@
1
+ import { useEffect, useState } from "react";
2
+ import { eventBus } from "../core/events";
3
+ import {
4
+ type ErrorRecoveryConfig,
5
+ type RetryContext,
6
+ errorRecoveryService,
7
+ } from "../services/error-recovery";
8
+ import type { RtcError } from "../state/types";
9
+
10
+ /**
11
+ * Error recovery status interface
12
+ */
13
+ export interface ErrorRecoveryStatus {
14
+ isRecovering: boolean;
15
+ activeRetries: Map<string, RetryContext>;
16
+ lastRecoveryAttempt?: {
17
+ error: RtcError;
18
+ attempts: number;
19
+ timestamp: number;
20
+ };
21
+ lastRecoveryResult?: {
22
+ error: RtcError;
23
+ attempts: number;
24
+ success: boolean;
25
+ timestamp: number;
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Hook for monitoring and controlling error recovery
31
+ *
32
+ * Provides real-time status of error recovery attempts and allows
33
+ * configuration of recovery behavior.
34
+ *
35
+ * @param config - Optional recovery configuration override
36
+ * @returns Error recovery status and control methods
37
+ *
38
+ * @example
39
+ * const {
40
+ * status,
41
+ * updateConfig,
42
+ * cancelRetry,
43
+ * cancelAllRetries
44
+ * } = useErrorRecovery();
45
+ *
46
+ * // Monitor recovery status
47
+ * if (status.isRecovering) {
48
+ * // Handle recovery state
49
+ * }
50
+ *
51
+ * // Configure recovery behavior
52
+ * updateConfig({ maxRetries: 5, retryDelay: 2000 });
53
+ */
54
+ export function useErrorRecovery(config?: Partial<ErrorRecoveryConfig>) {
55
+ const [status, setStatus] = useState<ErrorRecoveryStatus>({
56
+ isRecovering: false,
57
+ activeRetries: new Map(),
58
+ });
59
+
60
+ useEffect(() => {
61
+ // Apply config if provided
62
+ if (config) {
63
+ errorRecoveryService.updateConfig(config);
64
+ }
65
+
66
+ // Listen for recovery events
67
+ const recoveryAttemptSub = eventBus.on("recovery:attempt", (event) => {
68
+ setStatus((prev) => ({
69
+ ...prev,
70
+ isRecovering: true,
71
+ lastRecoveryAttempt: event.payload,
72
+ activeRetries: errorRecoveryService.getActiveRetries(),
73
+ }));
74
+ });
75
+
76
+ const recoverySuccessSub = eventBus.on("recovery:success", (event) => {
77
+ setStatus((prev) => ({
78
+ ...prev,
79
+ isRecovering: false,
80
+ lastRecoveryResult: {
81
+ ...event.payload,
82
+ success: true,
83
+ },
84
+ activeRetries: errorRecoveryService.getActiveRetries(),
85
+ }));
86
+ });
87
+
88
+ const recoveryFailedSub = eventBus.on("recovery:failed", (event) => {
89
+ setStatus((prev) => ({
90
+ ...prev,
91
+ isRecovering: false,
92
+ lastRecoveryResult: {
93
+ ...event.payload,
94
+ success: false,
95
+ },
96
+ activeRetries: errorRecoveryService.getActiveRetries(),
97
+ }));
98
+ });
99
+
100
+ // Update active retries periodically
101
+ const updateInterval = setInterval(() => {
102
+ setStatus((prev) => ({
103
+ ...prev,
104
+ activeRetries: errorRecoveryService.getActiveRetries(),
105
+ isRecovering: errorRecoveryService.getActiveRetries().size > 0,
106
+ }));
107
+ }, 1000);
108
+
109
+ return () => {
110
+ recoveryAttemptSub.unsubscribe();
111
+ recoverySuccessSub.unsubscribe();
112
+ recoveryFailedSub.unsubscribe();
113
+ clearInterval(updateInterval);
114
+ };
115
+ }, [config]);
116
+
117
+ const updateConfig = (newConfig: Partial<ErrorRecoveryConfig>) => {
118
+ errorRecoveryService.updateConfig(newConfig);
119
+ };
120
+
121
+ const cancelRetry = (retryKey: string) => {
122
+ const cancelled = errorRecoveryService.cancelRetry(retryKey);
123
+ if (cancelled) {
124
+ setStatus((prev) => ({
125
+ ...prev,
126
+ activeRetries: errorRecoveryService.getActiveRetries(),
127
+ isRecovering: errorRecoveryService.getActiveRetries().size > 0,
128
+ }));
129
+ }
130
+ return cancelled;
131
+ };
132
+
133
+ const cancelAllRetries = () => {
134
+ errorRecoveryService.cancelAllRetries();
135
+ setStatus((prev) => ({
136
+ ...prev,
137
+ activeRetries: new Map(),
138
+ isRecovering: false,
139
+ }));
140
+ };
141
+
142
+ return {
143
+ status,
144
+ updateConfig,
145
+ cancelRetry,
146
+ cancelAllRetries,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Hook for monitoring recovery of a specific error type
152
+ *
153
+ * @param errorCode - The error code to monitor
154
+ * @returns Recovery status for the specific error type
155
+ */
156
+ export function useErrorRecoveryForType(errorCode: string) {
157
+ const [isRecovering, setIsRecovering] = useState(false);
158
+ const [lastAttempt, setLastAttempt] = useState<number>(0);
159
+ const [lastResult, setLastResult] = useState<{
160
+ success: boolean;
161
+ timestamp: number;
162
+ } | null>(null);
163
+
164
+ useEffect(() => {
165
+ const recoveryAttemptSub = eventBus.on("recovery:attempt", (event) => {
166
+ if (event.payload.error.code === errorCode) {
167
+ setIsRecovering(true);
168
+ setLastAttempt(event.payload.attempts);
169
+ }
170
+ });
171
+
172
+ const recoverySuccessSub = eventBus.on("recovery:success", (event) => {
173
+ if (event.payload.error.code === errorCode) {
174
+ setIsRecovering(false);
175
+ setLastResult({ success: true, timestamp: event.payload.timestamp });
176
+ }
177
+ });
178
+
179
+ const recoveryFailedSub = eventBus.on("recovery:failed", (event) => {
180
+ if (event.payload.error.code === errorCode) {
181
+ setIsRecovering(false);
182
+ setLastResult({ success: false, timestamp: event.payload.timestamp });
183
+ }
184
+ });
185
+
186
+ return () => {
187
+ recoveryAttemptSub.unsubscribe();
188
+ recoverySuccessSub.unsubscribe();
189
+ recoveryFailedSub.unsubscribe();
190
+ };
191
+ }, [errorCode]);
192
+
193
+ return {
194
+ isRecovering,
195
+ lastAttempt,
196
+ lastResult,
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Hook for automatic graceful degradation
202
+ *
203
+ * Automatically handles graceful degradation scenarios like
204
+ * falling back to audio-only when video fails.
205
+ *
206
+ * @param degradationConfig - Configuration for degradation behavior
207
+ */
208
+ export function useGracefulDegradation(degradationConfig?: {
209
+ enableAudioOnlyFallback?: boolean;
210
+ enableLowerQualityFallback?: boolean;
211
+ notifyUser?: boolean;
212
+ }) {
213
+ const config = {
214
+ enableAudioOnlyFallback: true,
215
+ enableLowerQualityFallback: true,
216
+ notifyUser: true,
217
+ ...degradationConfig,
218
+ };
219
+
220
+ const [degradationStatus, setDegradationStatus] = useState({
221
+ isAudioOnly: false,
222
+ isLowerQuality: false,
223
+ reason: null as string | null,
224
+ });
225
+
226
+ useEffect(() => {
227
+ // Listen for media failures that might trigger degradation
228
+ const mediaDisabledSub = eventBus.on("media:disabled", (event) => {
229
+ if (
230
+ event.payload.mediaType === "video" &&
231
+ config.enableAudioOnlyFallback
232
+ ) {
233
+ setDegradationStatus((prev) => ({
234
+ ...prev,
235
+ isAudioOnly: true,
236
+ reason: "video_disabled",
237
+ }));
238
+
239
+ if (config.notifyUser) {
240
+ // Could emit a user notification event here
241
+ eventBus.emit(
242
+ "degradation:audio-only",
243
+ {
244
+ reason: "video_disabled",
245
+ timestamp: Date.now(),
246
+ },
247
+ "user"
248
+ );
249
+ }
250
+ }
251
+ });
252
+
253
+ // Listen for connection quality changes that might trigger degradation
254
+ const qualityChangedSub = eventBus.on(
255
+ "connection:quality-changed",
256
+ (event) => {
257
+ if (
258
+ event.payload.quality === "poor" &&
259
+ config.enableLowerQualityFallback
260
+ ) {
261
+ setDegradationStatus((prev) => ({
262
+ ...prev,
263
+ isLowerQuality: true,
264
+ reason: "poor_connection",
265
+ }));
266
+
267
+ if (config.notifyUser) {
268
+ eventBus.emit(
269
+ "degradation:lower-quality",
270
+ {
271
+ reason: "poor_connection",
272
+ timestamp: Date.now(),
273
+ },
274
+ "user"
275
+ );
276
+ }
277
+ }
278
+ }
279
+ );
280
+
281
+ return () => {
282
+ mediaDisabledSub.unsubscribe();
283
+ qualityChangedSub.unsubscribe();
284
+ };
285
+ }, [config]);
286
+
287
+ const resetDegradation = () => {
288
+ setDegradationStatus({
289
+ isAudioOnly: false,
290
+ isLowerQuality: false,
291
+ reason: null,
292
+ });
293
+ };
294
+
295
+ return {
296
+ degradationStatus,
297
+ resetDegradation,
298
+ };
299
+ }
@@ -0,0 +1,84 @@
1
+ import { useCallback } from "react";
2
+ import { clearErrors } from "../state/errors";
3
+ import type { ErrorCode, RtcError } from "../state/errors";
4
+ import { useRtcStore } from "../state/store";
5
+
6
+ export interface UseErrorsReturn {
7
+ errors: RtcError[];
8
+ clearAll: () => void;
9
+ clearByCode: (code: ErrorCode) => void;
10
+ clearByPredicate: (predicate: (error: RtcError) => boolean) => void;
11
+ hasErrors: boolean;
12
+ errorCount: number;
13
+ latestError: RtcError | undefined;
14
+ }
15
+
16
+ /**
17
+ * Hook for apps to consume and manage SDK errors
18
+ *
19
+ * Provides read access to all errors and methods to clear them.
20
+ * Perfect for implementing toast notifications, error logging, and telemetry.
21
+ *
22
+ * @example
23
+ * const { errors, clearAll, hasErrors } = useErrors();
24
+ *
25
+ * // Show toast for new errors
26
+ * useEffect(() => {
27
+ * if (hasErrors) {
28
+ * showToast(errors[errors.length - 1].message);
29
+ * }
30
+ * }, [errors, hasErrors]);
31
+ */
32
+ export function useErrors(): UseErrorsReturn {
33
+ // Subscribe to errors array in store
34
+ const errors = useRtcStore((state) => state.errors);
35
+
36
+ // Clear all errors
37
+ const clearAll = useCallback(() => {
38
+ clearErrors();
39
+ }, []);
40
+
41
+ // Clear errors by code
42
+ const clearByCode = useCallback((code: ErrorCode) => {
43
+ clearErrors((error) => error.code === code);
44
+ }, []);
45
+
46
+ // Clear errors by custom predicate
47
+ const clearByPredicate = useCallback(
48
+ (predicate: (error: RtcError) => boolean) => {
49
+ clearErrors(predicate);
50
+ },
51
+ []
52
+ );
53
+
54
+ return {
55
+ errors,
56
+ clearAll,
57
+ clearByCode,
58
+ clearByPredicate,
59
+ hasErrors: errors.length > 0,
60
+ errorCount: errors.length,
61
+ latestError: errors[errors.length - 1] || undefined,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Hook to get errors of specific types
67
+ * Useful for filtering errors by category
68
+ *
69
+ * @example
70
+ * const deviceErrors = useErrorsByCode(['DEVICE_SWITCH', 'MEDIA_PERMISSION']);
71
+ */
72
+ export function useErrorsByCode(codes: ErrorCode[]): RtcError[] {
73
+ return useRtcStore((state) =>
74
+ state.errors.filter((error) => codes.includes(error.code as ErrorCode))
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Hook to get the count of errors by code
80
+ * Useful for badges and indicators
81
+ */
82
+ export function useErrorCount(): number {
83
+ return useRtcStore((state) => state.errors.length);
84
+ }