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,135 @@
1
+ import type {
2
+ ConnectionQuality,
3
+ DataPacket_Kind,
4
+ Participant,
5
+ Room,
6
+ RoomOptions,
7
+ Track,
8
+ TrackPublication,
9
+ } from "livekit-client";
10
+
11
+ export interface LiveKitConnectionConfig {
12
+ url: string;
13
+ token: string;
14
+ options?: RoomOptions;
15
+ }
16
+
17
+ export interface LiveKitMediaConfig {
18
+ audio: {
19
+ enabled: boolean;
20
+ deviceId?: string;
21
+ };
22
+ video: {
23
+ enabled: boolean;
24
+ deviceId?: string;
25
+ };
26
+ screen?: {
27
+ enabled: boolean;
28
+ };
29
+ }
30
+
31
+ export interface LiveKitParticipant extends Participant {
32
+ displayName?: string;
33
+ avatarUrl?: string;
34
+ }
35
+
36
+ export interface LiveKitTrackInfo {
37
+ track: Track;
38
+ participant: LiveKitParticipant;
39
+ publication: TrackPublication;
40
+ isLocal: boolean;
41
+ kind: Track.Kind;
42
+ }
43
+
44
+ export interface LiveKitEvents {
45
+ participantConnected: { participant: LiveKitParticipant };
46
+ participantDisconnected: { participant: LiveKitParticipant };
47
+ trackSubscribed: { trackInfo: LiveKitTrackInfo };
48
+ trackUnsubscribed: { trackInfo: LiveKitTrackInfo };
49
+ trackMuted: { trackInfo: LiveKitTrackInfo };
50
+ trackUnmuted: { trackInfo: LiveKitTrackInfo };
51
+ connectionQualityChanged: {
52
+ participant: LiveKitParticipant;
53
+ quality: ConnectionQuality;
54
+ };
55
+ dataReceived: {
56
+ data: Uint8Array;
57
+ participant?: LiveKitParticipant;
58
+ kind: DataPacket_Kind;
59
+ };
60
+ }
61
+
62
+ export interface MediaActions {
63
+ enableCamera: () => Promise<void>;
64
+ disableCamera: () => Promise<void>;
65
+ enableMicrophone: () => Promise<void>;
66
+ disableMicrophone: () => Promise<void>;
67
+ toggleCamera: () => Promise<void>;
68
+ toggleMicrophone: () => Promise<void>;
69
+ enableScreenShare: () => Promise<void>;
70
+ disableScreenShare: () => Promise<void>;
71
+ toggleScreenShare: () => Promise<void>;
72
+ }
73
+
74
+ export interface LiveKitServiceOptions {
75
+ log:
76
+ | ((
77
+ lvl: "debug" | "info" | "warn" | "error",
78
+ msg: string,
79
+ extra?: any
80
+ ) => void)
81
+ | undefined;
82
+ }
83
+
84
+ /**
85
+ * RPC (Remote Procedure Call) related types and interfaces
86
+ */
87
+ export interface RpcMethodHandler<TReq = any, TRes = any> {
88
+ method: string;
89
+ handler: (data: TReq, caller: Participant) => Promise<TRes> | TRes;
90
+ }
91
+
92
+ export interface RpcCallOptions {
93
+ /**
94
+ * Timeout for the RPC call in milliseconds
95
+ */
96
+ timeout?: number;
97
+ /**
98
+ * Whether to wait for a response
99
+ */
100
+ waitForResponse?: boolean;
101
+ }
102
+
103
+ export interface RpcManager {
104
+ /**
105
+ * Register an RPC method handler
106
+ */
107
+ registerMethod<TReq = any, TRes = any>(
108
+ method: string,
109
+ handler: (data: TReq, caller: Participant) => Promise<TRes> | TRes
110
+ ): void;
111
+
112
+ /**
113
+ * Unregister an RPC method handler
114
+ */
115
+ unregisterMethod(method: string): void;
116
+
117
+ /**
118
+ * Call an RPC method on a remote participant
119
+ */
120
+ callMethod<TReq = any, TRes = any>(
121
+ destinationIdentity: string,
122
+ method: string,
123
+ data: TReq,
124
+ options?: RpcCallOptions
125
+ ): Promise<TRes>;
126
+
127
+ /**
128
+ * Call an RPC method on all participants
129
+ */
130
+ broadcastMethod<TReq = any>(
131
+ method: string,
132
+ data: TReq,
133
+ options?: Omit<RpcCallOptions, "waitForResponse">
134
+ ): Promise<void>;
135
+ }
@@ -0,0 +1,78 @@
1
+ import React, { createContext, useContext, useEffect, useMemo } from "react";
2
+ import type { Nullable } from "../core";
3
+ import { type RtcSdk, type SdkBuildOptions, buildSdk } from "../services";
4
+ import { rtcStore } from "../state/store";
5
+
6
+ export type RtcOptions = SdkBuildOptions;
7
+ export type { RtcSdk };
8
+
9
+ const RtcContext = createContext<Nullable<RtcSdk>>(null);
10
+
11
+ export function RtcProvider({
12
+ options,
13
+ children,
14
+ }: {
15
+ options: RtcOptions;
16
+ children: React.ReactNode;
17
+ }) {
18
+ const sdk = useMemo(() => buildSdk(options), [options]);
19
+
20
+ useEffect(() => {
21
+ // Configure API first
22
+ try {
23
+ sdk.configureApi({
24
+ baseUrl: options.signalHost,
25
+ token: async () => {
26
+ const token = sdk.auth.getCurrentToken();
27
+ return token || "";
28
+ },
29
+ });
30
+ options.log?.("info", "API configured successfully");
31
+ } catch (error) {
32
+ options.log?.("error", "Failed to configure API", error);
33
+ rtcStore.getState().addError({
34
+ code: "API_CONFIG_ERROR",
35
+ message: "Failed to configure API",
36
+ timestamp: Date.now(),
37
+ context: error,
38
+ });
39
+ }
40
+
41
+ // Initialize socket connection with livekit service
42
+ sdk.socket
43
+ .initialize(
44
+ options.signalHost,
45
+ sdk.auth,
46
+ {
47
+ reconnectAttempts: 5,
48
+ reconnectDelay: 1000,
49
+ },
50
+ sdk.livekit,
51
+ sdk.autoJoinConfig
52
+ )
53
+ .catch((error) => {
54
+ options.log?.("error", "Failed to initialize socket connection", error);
55
+
56
+ rtcStore.getState().addError({
57
+ code: "SOCKET_INIT_ERROR",
58
+ message: "Failed to initialize socket connection",
59
+ timestamp: Date.now(),
60
+ context: error,
61
+ });
62
+ });
63
+
64
+ return () => {
65
+ sdk.cleanup();
66
+ };
67
+ }, [sdk, options]);
68
+
69
+ return React.createElement(RtcContext.Provider, { value: sdk }, children);
70
+ }
71
+
72
+ export const useSdk = (): RtcSdk => {
73
+ const ctx = useContext(RtcContext);
74
+ if (!ctx) {
75
+ throw new Error("useSdk must be used within RtcProvider");
76
+ }
77
+ return ctx;
78
+ };
@@ -0,0 +1,260 @@
1
+ import type {
2
+ CallActionResponse,
3
+ CallResponse,
4
+ InitiateCallParams,
5
+ SignalClient,
6
+ } from "../core/signal";
7
+ import type { AuthManager } from "../core/auth.manager";
8
+ import { SdkEventType, eventBus } from "../core/events";
9
+ import { pushLiveKitConnectError } from "../state/errors";
10
+ import { rtcStore } from "../state/store";
11
+ import type { SessionStatus } from "../state/types";
12
+ import { createLogger } from "../utils/logger";
13
+
14
+ export interface CallActions {
15
+ initiate: (params: InitiateCallParams) => Promise<CallResponse>;
16
+ accept: (callId: string) => Promise<CallActionResponse>;
17
+ decline: (callId: string, reason?: string) => Promise<CallActionResponse>;
18
+ leave: (callId: string) => Promise<CallActionResponse>;
19
+ join: () => Promise<void>;
20
+ }
21
+
22
+ export function createCallActions(signal: SignalClient, auth: AuthManager, livekit?: any): CallActions {
23
+ const logger = createLogger("call-actions");
24
+ async function initiate(params: InitiateCallParams): Promise<CallResponse> {
25
+ const response = await signal.initiate(params);
26
+
27
+ rtcStore.getState().patch((state) => {
28
+ state.session = {
29
+ id: response.id,
30
+ status: "CALLING", // Caller initiated, waiting for acceptance
31
+ mode: response.mode as "AUDIO" | "VIDEO",
32
+ // Identity context: I initiated this call, so I'm the caller
33
+ myRole: "CALLER",
34
+ initiatedByMe: true,
35
+ };
36
+
37
+ // Use participants from API response instead of request params
38
+ for (const participant of response.participants) {
39
+ const isCaller = participant.userId === response.callerId;
40
+ // Use userId as the key since that's what auth.getCurrentUserId() returns
41
+ const participantData: any = {
42
+ id: participant.userId, // Store the user ID as the participant ID
43
+ role: isCaller ? "CALLER" : "MEMBER",
44
+ callState: isCaller ? "JOINED" : "INVITED", // Caller is already in the call
45
+ invitedAt: Date.now(),
46
+ audioEnabled: true,
47
+ videoEnabled: true,
48
+ isSpeaking: false,
49
+ };
50
+
51
+ // Set joinedAt only for caller
52
+ if (isCaller) {
53
+ participantData.joinedAt = Date.now();
54
+ }
55
+
56
+ state.room.participants[participant.userId] = participantData;
57
+
58
+ logger.debug("Created participant during call initiation", {
59
+ participantId: participant.userId,
60
+ role: isCaller ? "CALLER" : "MEMBER",
61
+ callState: "INVITED",
62
+ callId: response.id,
63
+ });
64
+ }
65
+ });
66
+
67
+ return response;
68
+ }
69
+
70
+ async function accept(callId: string): Promise<CallActionResponse> {
71
+ const response = await signal.accept(callId);
72
+
73
+ rtcStore.getState().patch((state) => {
74
+ state.session = {
75
+ ...state.session,
76
+ id: callId,
77
+ status: "ACCEPTED", // Call accepted but not yet joined media
78
+ // Identity context: I accepted this call, so I'm the callee
79
+ myRole: "CALLEE",
80
+ initiatedByMe: false,
81
+ };
82
+ // Clear incoming call
83
+ state.incomingCall = undefined;
84
+
85
+ // Note: Self presence will be updated via socket events from backend
86
+ // The backend will emit call.accepted event with participant info
87
+ });
88
+
89
+ return response;
90
+ }
91
+
92
+ async function decline(
93
+ callId: string,
94
+ reason?: string
95
+ ): Promise<CallActionResponse> {
96
+ logger.debug("Starting decline action", { callId, reason });
97
+
98
+ try {
99
+ const response = await signal.decline(callId);
100
+ logger.info("Decline API success", { callId, response });
101
+
102
+ rtcStore.getState().patch((state) => {
103
+ if (state.session.id === callId) {
104
+ state.session.status = response.state as SessionStatus;
105
+ }
106
+ // Clear incoming call
107
+ state.incomingCall = undefined;
108
+ logger.debug("Cleared incomingCall state");
109
+ });
110
+
111
+ return response;
112
+ } catch (error) {
113
+ logger.error("Decline API failed", { callId, error });
114
+
115
+ // Even if API fails, clear the incoming call to prevent stuck modal
116
+ rtcStore.getState().patch((state) => {
117
+ state.incomingCall = undefined;
118
+ state.session.status = "IDLE";
119
+ logger.warn("Force-cleared state due to API failure");
120
+ });
121
+
122
+ throw error;
123
+ }
124
+ }
125
+
126
+ async function leave(callId: string): Promise<CallActionResponse> {
127
+ const response = await signal.leave(callId);
128
+
129
+ // Note: Don't update local state here - let socket events handle it
130
+ // Backend will decide whether to end the call or just mark participant as left
131
+ // and emit appropriate socket events (call.participant-left vs call.ended)
132
+
133
+ return response;
134
+ }
135
+
136
+ async function join(): Promise<void> {
137
+ const currentState = rtcStore.getState();
138
+ const joinInfo = currentState.session.livekitInfo;
139
+
140
+ if (!joinInfo) {
141
+ throw new Error("No join info available - cannot join call");
142
+ }
143
+
144
+ if (!joinInfo.url) {
145
+ throw new Error("No LiveKit URL available - cannot join call");
146
+ }
147
+
148
+ if (!livekit) {
149
+ throw new Error("LiveKit service not available");
150
+ }
151
+
152
+ if (currentState.session.status === "ACTIVE") {
153
+ logger.warn("Already in active call, ignoring join request");
154
+ return;
155
+ }
156
+
157
+ // Get current user ID from auth instead of localParticipantId
158
+ const currentUserId = auth.getCurrentUserId();
159
+
160
+ try {
161
+ logger.info("Manually joining LiveKit room", {
162
+ callId: joinInfo.callId,
163
+ currentUserId,
164
+ roomName: joinInfo.roomName,
165
+ });
166
+
167
+ // Update state to connecting
168
+ rtcStore.getState().patch((state) => {
169
+ state.session.status = "CONNECTING";
170
+ });
171
+
172
+ await livekit.joinRoom(joinInfo.token, joinInfo.url);
173
+
174
+ // Update state after successful join
175
+ rtcStore.getState().patch((state) => {
176
+ state.session.status = "ACTIVE";
177
+ if (currentUserId) {
178
+ // Defensive check: create participant if it doesn't exist
179
+ if (!state.room.participants[currentUserId]) {
180
+ logger.warn("Creating missing participant during manual join", {
181
+ currentUserId,
182
+ callId: joinInfo.callId,
183
+ });
184
+
185
+ state.room.participants[currentUserId] = {
186
+ id: currentUserId,
187
+ firstName: `User ${currentUserId}`,
188
+ role: state.session.myRole || "MEMBER",
189
+ callState: "INVITED",
190
+ invitedAt: Date.now(),
191
+ audioEnabled: true,
192
+ videoEnabled: true,
193
+ isSpeaking: false,
194
+ };
195
+ }
196
+
197
+ state.room.participants[currentUserId].callState = "JOINED";
198
+ state.room.participants[currentUserId].joinedAt = Date.now();
199
+
200
+ logger.debug("Participant joined during manual join", {
201
+ participantId: currentUserId,
202
+ callState: "JOINED",
203
+ callId: joinInfo.callId,
204
+ });
205
+ }
206
+ });
207
+
208
+ // Emit participant joined event using session role context
209
+ eventBus.emit(
210
+ SdkEventType.PARTICIPANT_JOINED,
211
+ {
212
+ callId: joinInfo.callId,
213
+ participant: {
214
+ id: currentUserId || "unknown",
215
+ role: currentState.session.myRole || "CALLEE",
216
+ },
217
+ timestamp: Date.now(),
218
+ },
219
+ "user"
220
+ );
221
+
222
+ logger.info("Successfully joined LiveKit room manually", {
223
+ callId: joinInfo.callId,
224
+ currentUserId,
225
+ });
226
+
227
+ } catch (error) {
228
+ logger.error("Failed to manually join LiveKit room", {
229
+ callId: joinInfo.callId,
230
+ error,
231
+ });
232
+
233
+ // Reset state on failure
234
+ rtcStore.getState().patch((state) => {
235
+ state.session.status = "READY_TO_JOIN";
236
+ if (currentUserId) {
237
+ // Defensive check: only update if participant exists
238
+ if (state.room.participants[currentUserId]) {
239
+ state.room.participants[currentUserId].callState = "LEFT";
240
+ }
241
+ }
242
+ });
243
+
244
+ pushLiveKitConnectError(
245
+ error instanceof Error ? error.message : "Unknown error",
246
+ error
247
+ );
248
+
249
+ throw error;
250
+ }
251
+ }
252
+
253
+ return {
254
+ initiate,
255
+ accept,
256
+ decline,
257
+ leave,
258
+ join,
259
+ };
260
+ }