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.
- package/.azure-pipelines/publish-public.yml +37 -0
- package/.azure-pipelines/publish.yml +39 -0
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/AUTO_JOIN_GUIDE.md +411 -0
- package/README.md +215 -0
- package/Screenshot 2025-09-24 at 14.34.48.png +0 -0
- package/Screenshot 2025-10-04 at 12.58.54.png +0 -0
- package/biome.json +48 -0
- package/examples/demo/.env.example +19 -0
- package/examples/demo/CHANGELOG.md +22 -0
- package/examples/demo/README.md +72 -0
- package/examples/demo/eslint.config.js +23 -0
- package/examples/demo/index.html +13 -0
- package/examples/demo/package.json +34 -0
- package/examples/demo/pnpm-lock.yaml +2098 -0
- package/examples/demo/pnpm-workspace.yaml +1 -0
- package/examples/demo/public/vite.svg +1 -0
- package/examples/demo/src/App.css +52 -0
- package/examples/demo/src/App.tsx +176 -0
- package/examples/demo/src/assets/react.svg +1 -0
- package/examples/demo/src/components/auth/LoginForm.css +144 -0
- package/examples/demo/src/components/auth/LoginForm.tsx +80 -0
- package/examples/demo/src/components/calling/AutoJoinSettings.tsx +213 -0
- package/examples/demo/src/components/calling/AutoJoinStatus.tsx +72 -0
- package/examples/demo/src/components/calling/CallInitiator.css +258 -0
- package/examples/demo/src/components/calling/CallInitiator.tsx +142 -0
- package/examples/demo/src/components/calling/CallNotifications.css +119 -0
- package/examples/demo/src/components/calling/CallNotifications.tsx +108 -0
- package/examples/demo/src/components/calling/IncomingCallModal.css +192 -0
- package/examples/demo/src/components/calling/IncomingCallModal.tsx +78 -0
- package/examples/demo/src/components/calling/MinimizedCall.css +156 -0
- package/examples/demo/src/components/calling/MinimizedCall.tsx +78 -0
- package/examples/demo/src/components/conference/ConferenceHeader.css +265 -0
- package/examples/demo/src/components/conference/ConferenceHeader.tsx +78 -0
- package/examples/demo/src/components/conference/EnhancedControlBar.css +356 -0
- package/examples/demo/src/components/conference/EnhancedControlBar.tsx +262 -0
- package/examples/demo/src/components/conference/PaginationControls.css +67 -0
- package/examples/demo/src/components/conference/PaginationControls.tsx +64 -0
- package/examples/demo/src/components/conference/ParticipantGrid.css +153 -0
- package/examples/demo/src/components/conference/ParticipantGrid.tsx +87 -0
- package/examples/demo/src/components/conference/ParticipantTile.css +210 -0
- package/examples/demo/src/components/conference/ParticipantTile.tsx +114 -0
- package/examples/demo/src/components/conference/VideoConference.css +214 -0
- package/examples/demo/src/components/conference/VideoConference.tsx +93 -0
- package/examples/demo/src/contexts/AuthContext.tsx +105 -0
- package/examples/demo/src/hooks/useAuth.ts +5 -0
- package/examples/demo/src/hooks/useCallTimer.ts +42 -0
- package/examples/demo/src/index.css +68 -0
- package/examples/demo/src/main.tsx +10 -0
- package/examples/demo/src/services/auth.service.ts +153 -0
- package/examples/demo/src/types/auth.types.ts +31 -0
- package/examples/demo/tsconfig.app.json +28 -0
- package/examples/demo/tsconfig.json +7 -0
- package/examples/demo/tsconfig.node.json +26 -0
- package/examples/demo/vite.config.ts +15 -0
- package/images/callpad-without-ai.png +0 -0
- package/package.json +28 -0
- package/packages/sdk/CHANGELOG.md +33 -0
- package/packages/sdk/LICENSE +21 -0
- package/packages/sdk/README.md +97 -0
- package/packages/sdk/documentation.md +1132 -0
- package/packages/sdk/openapi-ts.config.ts +7 -0
- package/packages/sdk/package.json +88 -0
- package/packages/sdk/src/core/auth.manager.ts +52 -0
- package/packages/sdk/src/core/events/event-bus.ts +301 -0
- package/packages/sdk/src/core/events/index.ts +8 -0
- package/packages/sdk/src/core/events/types.ts +165 -0
- package/packages/sdk/src/core/index.ts +3 -0
- package/packages/sdk/src/core/signal/api.config.ts +49 -0
- package/packages/sdk/src/core/signal/index.ts +16 -0
- package/packages/sdk/src/core/signal/signal.client.ts +101 -0
- package/packages/sdk/src/core/signal/types.ts +110 -0
- package/packages/sdk/src/core/socketio/handlers/base.handler.ts +212 -0
- package/packages/sdk/src/core/socketio/handlers/call-accepted.handler.ts +34 -0
- package/packages/sdk/src/core/socketio/handlers/call-canceled.handler.ts +34 -0
- package/packages/sdk/src/core/socketio/handlers/call-declined.handler.ts +29 -0
- package/packages/sdk/src/core/socketio/handlers/call-ended.handler.ts +40 -0
- package/packages/sdk/src/core/socketio/handlers/call-incoming.handler.ts +72 -0
- package/packages/sdk/src/core/socketio/handlers/call-join-info.handler.ts +181 -0
- package/packages/sdk/src/core/socketio/handlers/call-participant-joined.handler.ts +42 -0
- package/packages/sdk/src/core/socketio/handlers/call-participant-joining.handler.ts +42 -0
- package/packages/sdk/src/core/socketio/handlers/call-timeout.handler.ts +31 -0
- package/packages/sdk/src/core/socketio/handlers/handler.registry.ts +62 -0
- package/packages/sdk/src/core/socketio/handlers/index.ts +21 -0
- package/packages/sdk/src/core/socketio/handlers/participant-left.handler.ts +37 -0
- package/packages/sdk/src/core/socketio/handlers/schema.ts +130 -0
- package/packages/sdk/src/core/socketio/index.ts +5 -0
- package/packages/sdk/src/core/socketio/socket.manager.ts +187 -0
- package/packages/sdk/src/core/socketio/types.ts +14 -0
- package/packages/sdk/src/core/types.ts +23 -0
- package/packages/sdk/src/generated/api/core/ApiError.ts +21 -0
- package/packages/sdk/src/generated/api/core/ApiRequestOptions.ts +13 -0
- package/packages/sdk/src/generated/api/core/ApiResult.ts +7 -0
- package/packages/sdk/src/generated/api/core/CancelablePromise.ts +126 -0
- package/packages/sdk/src/generated/api/core/OpenAPI.ts +55 -0
- package/packages/sdk/src/generated/api/core/request.ts +339 -0
- package/packages/sdk/src/generated/api/index.ts +5 -0
- package/packages/sdk/src/generated/api/models.ts +219 -0
- package/packages/sdk/src/generated/api/services.ts +225 -0
- package/packages/sdk/src/hooks/index.ts +21 -0
- package/packages/sdk/src/hooks/useAutoJoin.ts +66 -0
- package/packages/sdk/src/hooks/useCallActions.ts +28 -0
- package/packages/sdk/src/hooks/useCallQuality.ts +416 -0
- package/packages/sdk/src/hooks/useCallState.ts +23 -0
- package/packages/sdk/src/hooks/useConnection.ts +15 -0
- package/packages/sdk/src/hooks/useDevices.ts +296 -0
- package/packages/sdk/src/hooks/useErrorRecovery.ts +299 -0
- package/packages/sdk/src/hooks/useErrors.ts +84 -0
- package/packages/sdk/src/hooks/useEvent.ts +188 -0
- package/packages/sdk/src/hooks/useMediaControls.ts +215 -0
- package/packages/sdk/src/hooks/useParticipantStatus.ts +318 -0
- package/packages/sdk/src/hooks/useParticipants.ts +111 -0
- package/packages/sdk/src/index.ts +66 -0
- package/packages/sdk/src/livekit/constants.ts +76 -0
- package/packages/sdk/src/livekit/device.manager.ts +172 -0
- package/packages/sdk/src/livekit/error-classifier.ts +155 -0
- package/packages/sdk/src/livekit/events/eventBridge.ts +371 -0
- package/packages/sdk/src/livekit/events/trackRegistry.ts +114 -0
- package/packages/sdk/src/livekit/index.ts +49 -0
- package/packages/sdk/src/livekit/livekit.service.ts +110 -0
- package/packages/sdk/src/livekit/media.controls.ts +315 -0
- package/packages/sdk/src/livekit/room.manager.ts +79 -0
- package/packages/sdk/src/livekit/track.utils.ts +230 -0
- package/packages/sdk/src/livekit/types.ts +135 -0
- package/packages/sdk/src/provider/RtcProvider.tsx +78 -0
- package/packages/sdk/src/services/call-actions.ts +260 -0
- package/packages/sdk/src/services/error-recovery.ts +461 -0
- package/packages/sdk/src/services/index.ts +2 -0
- package/packages/sdk/src/services/sdk-builder.ts +104 -0
- package/packages/sdk/src/state/errors.ts +163 -0
- package/packages/sdk/src/state/selectors.ts +28 -0
- package/packages/sdk/src/state/store.ts +36 -0
- package/packages/sdk/src/state/types.ts +151 -0
- package/packages/sdk/src/utils/logger.ts +183 -0
- package/packages/sdk/tsconfig.json +49 -0
- package/packages/sdk/tsup.config.ts +51 -0
- package/pnpm-workspace.yaml +4 -0
- package/tsconfig.base.json +19 -0
- 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
|
+
}
|