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