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,111 @@
1
+ import { useRtcStore } from "../state/store";
2
+ import { useSdk } from "../provider/RtcProvider";
3
+ import type { Participant } from "../state/types";
4
+
5
+ type ParticipantKind = "active" | "missed" | "left";
6
+
7
+ interface UseParticipantsOptions {
8
+ page?: number;
9
+ pageSize?: number;
10
+ kind?: ParticipantKind;
11
+ }
12
+
13
+ export function useParticipants(callId?: string, options?: UseParticipantsOptions) {
14
+ const { page = 1, pageSize = 8, kind = "active" } = options || {};
15
+
16
+ return useRtcStore((state) => {
17
+ try {
18
+ const allParticipants = Object.values(state.room.participants) || [];
19
+
20
+ let filteredParticipants: Participant[] = [];
21
+ switch (kind) {
22
+ case "active":
23
+ filteredParticipants = allParticipants.filter((p) => p.callState === "JOINED");
24
+ break;
25
+ case "missed":
26
+ filteredParticipants = allParticipants.filter((p) => p.callState === "INVITED");
27
+ break;
28
+ case "left":
29
+ filteredParticipants = allParticipants.filter((p) => p.callState === "LEFT");
30
+ break;
31
+ default:
32
+ filteredParticipants = allParticipants.filter((p) => p.callState === "JOINED");
33
+ }
34
+
35
+ const activeParticipants = allParticipants.filter((p) => p.callState === "JOINED");
36
+ const pendingParticipants = allParticipants.filter(
37
+ (p) => p.callState === "INVITED" || p.callState === "RINGING"
38
+ );
39
+ const caller = allParticipants.find((p) => p.role === "CALLER") || null;
40
+
41
+ let localParticipant: Participant | null = null;
42
+ try {
43
+ const sdk = useSdk();
44
+ const currentUserId = sdk.auth.getCurrentUserId();
45
+ localParticipant = currentUserId ? state.room.participants[currentUserId] || null : null;
46
+ } catch {
47
+ localParticipant = null;
48
+ }
49
+
50
+ const totalParticipants = filteredParticipants.length;
51
+ const totalPages = Math.ceil(totalParticipants / pageSize);
52
+ const startIndex = (page - 1) * pageSize;
53
+ const endIndex = startIndex + pageSize;
54
+ const participants = filteredParticipants.slice(startIndex, endIndex);
55
+
56
+ return {
57
+ participants,
58
+ activeParticipants,
59
+ pendingParticipants,
60
+ caller,
61
+ localParticipant,
62
+ totalPages,
63
+ currentPage: page,
64
+ hasNextPage: page < totalPages,
65
+ hasPreviousPage: page > 1,
66
+ totalParticipants,
67
+ };
68
+ } catch (error) {
69
+ return {
70
+ participants: [],
71
+ activeParticipants: [],
72
+ pendingParticipants: [],
73
+ caller: null,
74
+ localParticipant: null,
75
+ totalPages: 1,
76
+ currentPage: 1,
77
+ hasNextPage: false,
78
+ hasPreviousPage: false,
79
+ totalParticipants: 0,
80
+ };
81
+ }
82
+ });
83
+ }
84
+
85
+ export function useParticipant(participantId: string): Participant | undefined {
86
+ return useRtcStore((state) => state.room.participants[participantId]);
87
+ }
88
+
89
+
90
+ export function useRingingParticipants(): Participant[] {
91
+ return useRtcStore((state) =>
92
+ Object.values(state.room.participants).filter(
93
+ (p) => p.callState === "RINGING"
94
+ )
95
+ );
96
+ }
97
+
98
+ export function useLocalParticipant(): Participant | undefined {
99
+ const sdk = useSdk();
100
+
101
+ return useRtcStore((state) => {
102
+ const currentUserId = sdk.auth.getCurrentUserId();
103
+ return currentUserId ? state.room.participants[currentUserId] : undefined;
104
+ });
105
+ }
106
+
107
+ export function useSpeakingParticipants(): Participant[] {
108
+ return useRtcStore((state) =>
109
+ Object.values(state.room.participants).filter((p) => p.isSpeaking)
110
+ );
111
+ }
@@ -0,0 +1,66 @@
1
+ // React integration - main SDK entry points
2
+ export {
3
+ RtcProvider,
4
+ useSdk,
5
+ type RtcOptions,
6
+ type RtcSdk,
7
+ } from "./provider/RtcProvider";
8
+
9
+ // React hooks for call management
10
+ export * from "./hooks";
11
+
12
+ // State selectors for advanced usage
13
+ export {
14
+ useParticipant,
15
+ useRingingParticipants,
16
+ useLocalParticipant,
17
+ useSpeakingParticipants,
18
+ } from "./state/selectors";
19
+
20
+ // Essential types consumers need
21
+ export type {
22
+ SessionStatus,
23
+ Participant,
24
+ PermissionStatus,
25
+ DeviceState,
26
+ IncomingCallInfo,
27
+ LiveKitJoinInfo,
28
+ RtcError,
29
+ RtcState,
30
+ } from "./state/types";
31
+
32
+ // Signal client types for call initiation
33
+ export type {
34
+ InitiateCallParams,
35
+ CallResponse,
36
+ CallActionResponse,
37
+ } from "./core/signal/types";
38
+
39
+ export type { CallJoinInfoEvent } from "./core/socketio/handlers/schema";
40
+
41
+ // Error management
42
+ export * from "./state/errors";
43
+
44
+ // API configuration
45
+ export { apiConfig, type ApiConfig } from "./core/signal/api.config";
46
+
47
+ // Event system for advanced usage
48
+ export { eventBus } from "./core/events";
49
+ export type {
50
+ SdkEvent,
51
+ SdkEventType,
52
+ EventHandler,
53
+ EventSubscription,
54
+ EventFilter,
55
+ CallInitiatedEvent,
56
+ CallIncomingEvent,
57
+ CallAcceptedEvent,
58
+ CallDeclinedEvent,
59
+ CallEndedEvent,
60
+ ParticipantJoinedEvent,
61
+ ParticipantLeftEvent,
62
+ MediaEnabledEvent,
63
+ MediaDisabledEvent,
64
+ ConnectionQualityChangedEvent,
65
+ ErrorOccurredEvent,
66
+ } from "./core/events/types";
@@ -0,0 +1,76 @@
1
+ import type {
2
+ ConnectionQuality,
3
+ ReconnectPolicy,
4
+ RoomOptions,
5
+ } from "livekit-client";
6
+ import { VideoPresets } from "livekit-client";
7
+
8
+ /**
9
+ * Production-ready room options with optimal settings
10
+ */
11
+ export const DEFAULT_ROOM_OPTIONS: RoomOptions = {
12
+ // Performance optimizations
13
+ adaptiveStream: true,
14
+ dynacast: true,
15
+
16
+ // Browser lifecycle handling
17
+ disconnectOnPageLeave: true,
18
+
19
+ // Reconnection handling with exponential backoff
20
+ reconnectPolicy: {
21
+ nextRetryDelayInMs: (context) => {
22
+ // Exponential backoff with jitter: base delay * 2^retryCount + random jitter
23
+ const baseDelay = 1000;
24
+ const maxDelay = 30000;
25
+ const delay = Math.min(baseDelay * 2 ** context.retryCount, maxDelay);
26
+ const jitter = Math.random() * 1000; // Add up to 1s jitter
27
+ return delay + jitter;
28
+ },
29
+ },
30
+
31
+ // Media capture defaults
32
+ videoCaptureDefaults: {
33
+ facingMode: "user",
34
+ resolution: VideoPresets.h720.resolution,
35
+ },
36
+ publishDefaults: {
37
+ videoSimulcastLayers: [
38
+ VideoPresets.h180,
39
+ VideoPresets.h360,
40
+ VideoPresets.h720,
41
+ ],
42
+ // stopLocalTrackOnUnpublish: true, // Not available in current LiveKit version
43
+ },
44
+ audioCaptureDefaults: {
45
+ autoGainControl: true,
46
+ echoCancellation: true,
47
+ noiseSuppression: true,
48
+ },
49
+
50
+ // Audio handling
51
+ webAudioMix: true,
52
+ };
53
+
54
+ export const TRACK_ATTACHMENT_CONFIG = {
55
+ maxRetries: 3,
56
+ retryDelay: 1000,
57
+ exponentialBackoff: true,
58
+ } as const;
59
+
60
+ /**
61
+ * Connection quality thresholds for network monitoring
62
+ */
63
+ export const CONNECTION_QUALITY_THRESHOLDS = {
64
+ excellent: { minScore: 5, label: "excellent" },
65
+ good: { minScore: 3, label: "good" },
66
+ poor: { minScore: 1, label: "poor" },
67
+ lost: { minScore: 0, label: "lost" },
68
+ } as const;
69
+
70
+ /**
71
+ * Screen share configuration
72
+ */
73
+ export const SCREEN_SHARE_CONFIG = {
74
+ video: true,
75
+ audio: true,
76
+ } as const;
@@ -0,0 +1,172 @@
1
+ import { ConnectionState, Room, RoomEvent, Track } from "livekit-client";
2
+ import { rtcStore } from "../state/store";
3
+ import { classifyMediaError } from "./error-classifier";
4
+
5
+ export class DeviceManager {
6
+ private cleanupFunctions: Array<() => void> = [];
7
+
8
+ constructor(private room: Room) {
9
+ this.setupDeviceEventListeners();
10
+ }
11
+
12
+ async enumerateDevices(): Promise<void> {
13
+ try {
14
+ const [cameras, microphones, speakers] = await Promise.all([
15
+ Room.getLocalDevices("videoinput"),
16
+ Room.getLocalDevices("audioinput"),
17
+ Room.getLocalDevices("audiooutput"),
18
+ ]);
19
+
20
+ rtcStore.getState().patch((state) => {
21
+ state.devices.cams = cameras;
22
+ state.devices.mics = microphones;
23
+ state.devices.speakers = speakers;
24
+ });
25
+ } catch (error) {
26
+ this.handleDeviceError("enumerate", error);
27
+ throw error;
28
+ }
29
+ }
30
+
31
+ async switchCamera(deviceId: string): Promise<void> {
32
+ if (this.room.state !== ConnectionState.Connected) {
33
+ throw new Error("Cannot switch camera - room not connected");
34
+ }
35
+
36
+ try {
37
+ await this.room.switchActiveDevice("videoinput", deviceId);
38
+
39
+ // Update selected device in state
40
+ rtcStore.getState().patch((state) => {
41
+ state.devices.selected.camId = deviceId;
42
+ });
43
+ } catch (error) {
44
+ // LiveKit automatically populates lastCameraError
45
+ const livekitError = this.room.localParticipant.lastCameraError;
46
+ this.handleDeviceError("camera", error, livekitError);
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ async switchMicrophone(deviceId: string): Promise<void> {
52
+ if (this.room.state !== ConnectionState.Connected) {
53
+ throw new Error("Cannot switch microphone - room not connected");
54
+ }
55
+
56
+ try {
57
+ await this.room.switchActiveDevice("audioinput", deviceId);
58
+
59
+ // Update selected device in state
60
+ rtcStore.getState().patch((state) => {
61
+ state.devices.selected.micId = deviceId;
62
+ });
63
+ } catch (error) {
64
+ // LiveKit automatically populates lastMicrophoneError
65
+ const livekitError = this.room.localParticipant.lastMicrophoneError;
66
+ this.handleDeviceError("microphone", error, livekitError);
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ async switchSpeaker(deviceId: string): Promise<void> {
72
+ if (this.room.state !== ConnectionState.Connected) {
73
+ throw new Error("Cannot switch speaker - room not connected");
74
+ }
75
+
76
+ try {
77
+ await this.room.switchActiveDevice("audiooutput", deviceId);
78
+
79
+ // Update selected device in state
80
+ rtcStore.getState().patch((state) => {
81
+ state.devices.selected.speakerId = deviceId;
82
+ });
83
+ } catch (error) {
84
+ // Note: speakers don't have a specific lastError in LiveKit
85
+ this.handleDeviceError("speaker", error);
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ async getCurrentDeviceSelection(): Promise<{
91
+ camera: string | undefined;
92
+ microphone: string | undefined;
93
+ speaker: string | undefined;
94
+ }> {
95
+ const videoTrack = this.room.localParticipant.getTrackPublication(
96
+ Track.Source.Camera
97
+ )?.track;
98
+ const audioTrack = this.room.localParticipant.getTrackPublication(
99
+ Track.Source.Microphone
100
+ )?.track;
101
+
102
+ const [cameraDeviceId, microphoneDeviceId] = await Promise.all([
103
+ videoTrack ? videoTrack.getDeviceId() : Promise.resolve(undefined),
104
+ audioTrack ? audioTrack.getDeviceId() : Promise.resolve(undefined),
105
+ ]);
106
+
107
+ return {
108
+ camera: cameraDeviceId || undefined,
109
+ microphone: microphoneDeviceId || undefined,
110
+ // Speaker device ID is not directly accessible from tracks
111
+ speaker: rtcStore.getState().devices.selected.speakerId || undefined,
112
+ };
113
+ }
114
+
115
+ private setupDeviceEventListeners(): void {
116
+ const handleDevicesChanged = () => {
117
+ // Refresh the device list when devices are added/removed
118
+ this.enumerateDevices().catch(() => {
119
+ // Silently handle refresh errors
120
+ });
121
+ };
122
+
123
+ // Listen for device errors
124
+ const handleDeviceError = (error: any) => {
125
+ this.handleDeviceError("device_event", error);
126
+ };
127
+
128
+ if ("MediaDevicesChanged" in RoomEvent) {
129
+ this.room.on(RoomEvent.MediaDevicesChanged, handleDevicesChanged);
130
+ }
131
+ if ("MediaDevicesError" in RoomEvent) {
132
+ this.room.on(RoomEvent.MediaDevicesError, handleDeviceError);
133
+ }
134
+
135
+ this.cleanupFunctions.push(() => {
136
+ if ("MediaDevicesChanged" in RoomEvent) {
137
+ this.room.off(RoomEvent.MediaDevicesChanged, handleDevicesChanged);
138
+ }
139
+ if ("MediaDevicesError" in RoomEvent) {
140
+ this.room.off(RoomEvent.MediaDevicesError, handleDeviceError);
141
+ }
142
+ });
143
+ }
144
+
145
+ private handleDeviceError(
146
+ operation: string,
147
+ error: unknown,
148
+ livekitError?: Error
149
+ ): void {
150
+ const mediaError = classifyMediaError(error, operation, livekitError);
151
+ rtcStore.getState().addError({
152
+ code: mediaError.code,
153
+ message: mediaError.message,
154
+ timestamp: Date.now(),
155
+ context: {
156
+ operation,
157
+ category: mediaError.category,
158
+ recoverable: mediaError.recoverable,
159
+ device: mediaError.device,
160
+ originalError: error,
161
+ livekitError,
162
+ },
163
+ });
164
+ }
165
+
166
+ destroy(): void {
167
+ for (const cleanup of this.cleanupFunctions) {
168
+ cleanup();
169
+ }
170
+ this.cleanupFunctions = [];
171
+ }
172
+ }
@@ -0,0 +1,155 @@
1
+ import { MediaDeviceFailure } from "livekit-client";
2
+
3
+ export interface MediaErrorInfo {
4
+ code: string;
5
+ userMessage: string;
6
+ recoverable: boolean;
7
+ category: "permission" | "device" | "unknown";
8
+ }
9
+
10
+ export abstract class MediaDeviceError extends Error {
11
+ abstract readonly code: string;
12
+ abstract readonly recoverable: boolean;
13
+ abstract readonly category: "permission" | "device" | "unknown";
14
+ readonly device: string;
15
+ readonly deviceName: string;
16
+ readonly cause: Error | undefined;
17
+
18
+ protected constructor(message: string, device: string, cause?: Error) {
19
+ super(message);
20
+ this.name = this.constructor.name;
21
+ this.device = device;
22
+ this.deviceName = device === "camera" ? "camera" : "microphone";
23
+ this.cause = cause;
24
+ }
25
+ }
26
+
27
+ export class MediaPermissionError extends MediaDeviceError {
28
+ readonly code: string;
29
+ readonly recoverable = true;
30
+ readonly category = "permission" as const;
31
+
32
+ constructor(message: string, device: string, cause?: Error) {
33
+ super(message, device, cause);
34
+ this.code =
35
+ device === "camera"
36
+ ? "CAMERA_PERMISSION_DENIED"
37
+ : "MICROPHONE_PERMISSION_DENIED";
38
+ }
39
+ }
40
+
41
+ export class MediaNotFoundError extends MediaDeviceError {
42
+ readonly code: string;
43
+ readonly recoverable = false;
44
+ readonly category = "device" as const;
45
+
46
+ constructor(message: string, device: string, cause?: Error) {
47
+ super(message, device, cause);
48
+ this.code =
49
+ device === "camera" ? "CAMERA_NOT_FOUND" : "MICROPHONE_NOT_FOUND";
50
+ }
51
+ }
52
+
53
+ export class MediaInUseError extends MediaDeviceError {
54
+ readonly code: string;
55
+ readonly recoverable = true;
56
+ readonly category = "device" as const;
57
+
58
+ constructor(message: string, device: string, cause?: Error) {
59
+ super(message, device, cause);
60
+ this.code = device === "camera" ? "CAMERA_IN_USE" : "MICROPHONE_IN_USE";
61
+ }
62
+ }
63
+
64
+ export class MediaUnknownError extends MediaDeviceError {
65
+ readonly code: string;
66
+ readonly recoverable = false;
67
+ readonly category = "unknown" as const;
68
+
69
+ constructor(message: string, device: string, cause?: Error) {
70
+ super(message, device, cause);
71
+ this.code =
72
+ device === "camera" ? "CAMERA_UNKNOWN_ERROR" : "MICROPHONE_UNKNOWN_ERROR";
73
+ }
74
+ }
75
+
76
+ export function classifyMediaError(
77
+ error: unknown,
78
+ device: string,
79
+ livekitError?: Error
80
+ ): MediaDeviceError {
81
+ // Try LiveKit classification first
82
+ const livekitFailure = MediaDeviceFailure.getFailure(error as Error);
83
+ if (livekitFailure) {
84
+ return createErrorFromFailure(livekitFailure, device, error as Error);
85
+ }
86
+
87
+ // Use LiveKit lastError if available
88
+ if (livekitError) {
89
+ const livekitFailureFromLastError =
90
+ MediaDeviceFailure.getFailure(livekitError);
91
+ if (livekitFailureFromLastError) {
92
+ return createErrorFromFailure(
93
+ livekitFailureFromLastError,
94
+ device,
95
+ livekitError
96
+ );
97
+ }
98
+ }
99
+
100
+ // Fall back to basic error analysis for unhandled cases
101
+ const cause = error instanceof Error ? error : undefined;
102
+ const message = cause?.message?.toLowerCase() || "";
103
+
104
+ if (
105
+ message.includes("permission") ||
106
+ message.includes("denied") ||
107
+ message.includes("notallowed")
108
+ ) {
109
+ return new MediaPermissionError("Permission denied", device, cause);
110
+ }
111
+
112
+ if (
113
+ message.includes("not found") ||
114
+ message.includes("unavailable") ||
115
+ message.includes("notfound")
116
+ ) {
117
+ return new MediaNotFoundError("Device not found", device, cause);
118
+ }
119
+
120
+ if (
121
+ message.includes("already in use") ||
122
+ message.includes("busy") ||
123
+ message.includes("in use")
124
+ ) {
125
+ return new MediaInUseError("Device in use", device, cause);
126
+ }
127
+
128
+ // Final fallback
129
+ return new MediaUnknownError(
130
+ cause ? `Unknown error: ${cause.message}` : "Unknown device error",
131
+ device,
132
+ cause
133
+ );
134
+ }
135
+
136
+ function createErrorFromFailure(
137
+ failure: MediaDeviceFailure,
138
+ device: string,
139
+ cause: Error
140
+ ): MediaDeviceError {
141
+ switch (failure) {
142
+ case MediaDeviceFailure.PermissionDenied:
143
+ return new MediaPermissionError("Permission denied", device, cause);
144
+ case MediaDeviceFailure.NotFound:
145
+ return new MediaNotFoundError("Device not found", device, cause);
146
+ case MediaDeviceFailure.DeviceInUse:
147
+ return new MediaInUseError("Device in use", device, cause);
148
+ default:
149
+ return new MediaUnknownError(
150
+ `LiveKit failure: ${failure}`,
151
+ device,
152
+ cause
153
+ );
154
+ }
155
+ }