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,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
|
+
}
|