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,188 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { eventBus } from "../core/events";
|
|
3
|
+
import { SdkEventType } from "../core/events/types";
|
|
4
|
+
import type { EventFilter, EventHandler, SdkEvent } from "../core/events/types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook for subscribing to SDK events
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Listen for specific event type
|
|
11
|
+
* const callEvent = useEvent(SdkEventType.CALL_ACCEPTED);
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Listen with callback
|
|
15
|
+
* useEvent(SdkEventType.MEDIA_ENABLED, (event) => {
|
|
16
|
+
* // Handle media enabled event
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // Listen to pattern with filter
|
|
21
|
+
* useEvent('call:*', null, (event) => event.payload.callId === 'specific-call');
|
|
22
|
+
*/
|
|
23
|
+
export function useEvent<T = any>(
|
|
24
|
+
eventType: string | SdkEventType,
|
|
25
|
+
callback?: EventHandler<T> | null,
|
|
26
|
+
filter?: EventFilter<T>
|
|
27
|
+
): SdkEvent<T> | undefined {
|
|
28
|
+
const [lastEvent, setLastEvent] = useState<SdkEvent<T> | undefined>(
|
|
29
|
+
undefined
|
|
30
|
+
);
|
|
31
|
+
const callbackRef = useRef(callback);
|
|
32
|
+
callbackRef.current = callback;
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const handler: EventHandler<T> = (event: SdkEvent<T>) => {
|
|
36
|
+
setLastEvent(event);
|
|
37
|
+
if (callbackRef.current) {
|
|
38
|
+
callbackRef.current(event);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Support pattern matching (e.g., "call:*")
|
|
43
|
+
const subscription = eventType.includes("*")
|
|
44
|
+
? eventBus.onPattern(eventType, handler, filter)
|
|
45
|
+
: eventBus.on(eventType, handler, filter);
|
|
46
|
+
|
|
47
|
+
return () => {
|
|
48
|
+
subscription.unsubscribe();
|
|
49
|
+
};
|
|
50
|
+
}, [eventType, filter]);
|
|
51
|
+
|
|
52
|
+
return lastEvent;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Hook for subscribing to events once
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* useEventOnce(SdkEventType.CALL_ACCEPTED, (event) => {
|
|
60
|
+
* // Handle call accepted event once
|
|
61
|
+
* });
|
|
62
|
+
*/
|
|
63
|
+
export function useEventOnce<T = any>(
|
|
64
|
+
eventType: string | SdkEventType,
|
|
65
|
+
callback: EventHandler<T>,
|
|
66
|
+
filter?: EventFilter<T>
|
|
67
|
+
): void {
|
|
68
|
+
const callbackRef = useRef(callback);
|
|
69
|
+
callbackRef.current = callback;
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const subscription = eventBus.once(eventType, callbackRef.current, filter);
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
subscription.unsubscribe();
|
|
76
|
+
};
|
|
77
|
+
}, [eventType, filter]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Hook for accessing the event bus directly
|
|
82
|
+
* Use this for advanced event management scenarios
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* const events = useEventBus();
|
|
86
|
+
*
|
|
87
|
+
* // Emit custom event
|
|
88
|
+
* events.emit('custom:event', { data: 'test' });
|
|
89
|
+
*
|
|
90
|
+
* // Get event history
|
|
91
|
+
* const history = events.getEventHistory();
|
|
92
|
+
*/
|
|
93
|
+
export function useEventBus() {
|
|
94
|
+
return eventBus;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Hook for getting events matching a condition
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* const callEvents = useEventHistory((event) =>
|
|
102
|
+
* event.type.startsWith('call:') &&
|
|
103
|
+
* event.payload.callId === currentCallId
|
|
104
|
+
* );
|
|
105
|
+
*/
|
|
106
|
+
export function useEventHistory<T = any>(
|
|
107
|
+
filter?: EventFilter<T>
|
|
108
|
+
): SdkEvent<T>[] {
|
|
109
|
+
const [events, setEvents] = useState<SdkEvent<T>[]>(() =>
|
|
110
|
+
filter ? eventBus.getEventsWhere(filter) : eventBus.getEventHistory()
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const updateEvents = () => {
|
|
115
|
+
const newEvents = filter
|
|
116
|
+
? eventBus.getEventsWhere(filter)
|
|
117
|
+
: eventBus.getEventHistory();
|
|
118
|
+
setEvents(newEvents);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Subscribe to any event to trigger updates
|
|
122
|
+
const subscription = eventBus.onPattern("*", updateEvents);
|
|
123
|
+
|
|
124
|
+
return () => {
|
|
125
|
+
subscription.unsubscribe();
|
|
126
|
+
};
|
|
127
|
+
}, [filter]);
|
|
128
|
+
|
|
129
|
+
return events;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Hook for consuming call-specific events
|
|
134
|
+
* Automatically filters events by call ID
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* const { callAccepted, callDeclined, participantJoined } = useCallEvents(callId);
|
|
138
|
+
*/
|
|
139
|
+
export function useCallEvents(callId?: string) {
|
|
140
|
+
const callFilter: EventFilter = (event) =>
|
|
141
|
+
!callId || event.payload?.callId === callId;
|
|
142
|
+
|
|
143
|
+
const callAccepted = useEvent(SdkEventType.CALL_ACCEPTED, null, callFilter);
|
|
144
|
+
const callDeclined = useEvent(SdkEventType.CALL_DECLINED, null, callFilter);
|
|
145
|
+
const callEnded = useEvent(SdkEventType.CALL_ENDED, null, callFilter);
|
|
146
|
+
const participantJoined = useEvent(
|
|
147
|
+
SdkEventType.PARTICIPANT_JOINED,
|
|
148
|
+
null,
|
|
149
|
+
callFilter
|
|
150
|
+
);
|
|
151
|
+
const participantLeft = useEvent(
|
|
152
|
+
SdkEventType.PARTICIPANT_LEFT,
|
|
153
|
+
null,
|
|
154
|
+
callFilter
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
callAccepted,
|
|
159
|
+
callDeclined,
|
|
160
|
+
callEnded,
|
|
161
|
+
participantJoined,
|
|
162
|
+
participantLeft,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Hook for consuming media events
|
|
168
|
+
* Automatically filters events by participant ID
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* const { mediaEnabled, mediaDisabled } = useMediaEvents(participantId);
|
|
172
|
+
*/
|
|
173
|
+
export function useMediaEvents(participantId?: string) {
|
|
174
|
+
const mediaFilter: EventFilter = (event) =>
|
|
175
|
+
!participantId || event.payload?.participantId === participantId;
|
|
176
|
+
|
|
177
|
+
const mediaEnabled = useEvent(SdkEventType.MEDIA_ENABLED, null, mediaFilter);
|
|
178
|
+
const mediaDisabled = useEvent(
|
|
179
|
+
SdkEventType.MEDIA_DISABLED,
|
|
180
|
+
null,
|
|
181
|
+
mediaFilter
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
mediaEnabled,
|
|
186
|
+
mediaDisabled,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { MediaActions } from "../livekit";
|
|
3
|
+
import { useSdk } from "../provider/RtcProvider";
|
|
4
|
+
import { useRtcStore } from "../state/store";
|
|
5
|
+
import type { RtcError } from "../state/types";
|
|
6
|
+
import { createLogger } from "../utils/logger";
|
|
7
|
+
import { useDevices } from "./useDevices";
|
|
8
|
+
|
|
9
|
+
const logger = createLogger("hooks:media-controls");
|
|
10
|
+
|
|
11
|
+
export interface MediaControlsState {
|
|
12
|
+
isVideoEnabled: boolean;
|
|
13
|
+
isAudioEnabled: boolean;
|
|
14
|
+
isCameraAvailable: boolean;
|
|
15
|
+
isMicrophoneAvailable: boolean;
|
|
16
|
+
isConnected: boolean;
|
|
17
|
+
isLoading: boolean;
|
|
18
|
+
errors: RtcError[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface EnhancedMediaActions {
|
|
22
|
+
enableCamera: () => Promise<void>;
|
|
23
|
+
disableCamera: () => Promise<void>;
|
|
24
|
+
enableMicrophone: () => Promise<void>;
|
|
25
|
+
disableMicrophone: () => Promise<void>;
|
|
26
|
+
toggleCamera: () => Promise<void>;
|
|
27
|
+
toggleMicrophone: () => Promise<void>;
|
|
28
|
+
// Device switching
|
|
29
|
+
switchCamera: (deviceId: string) => Promise<void>;
|
|
30
|
+
switchMicrophone: (deviceId: string) => Promise<void>;
|
|
31
|
+
// Simple aliases
|
|
32
|
+
toggleAudio: () => Promise<void>;
|
|
33
|
+
toggleVideo: () => Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MediaControlsHook
|
|
37
|
+
extends MediaControlsState,
|
|
38
|
+
EnhancedMediaActions {}
|
|
39
|
+
|
|
40
|
+
export function useMediaControls(): MediaControlsHook & {
|
|
41
|
+
devices: {
|
|
42
|
+
cameras: any[];
|
|
43
|
+
microphones: any[];
|
|
44
|
+
speakers: any[];
|
|
45
|
+
};
|
|
46
|
+
} {
|
|
47
|
+
const sdk = useSdk();
|
|
48
|
+
const local = useRtcStore((state) => state.local);
|
|
49
|
+
const connection = useRtcStore((state) => state.connection);
|
|
50
|
+
const errors = useRtcStore((state) =>
|
|
51
|
+
state.errors.filter(
|
|
52
|
+
(e) =>
|
|
53
|
+
e.code.startsWith("CAMERA_") ||
|
|
54
|
+
e.code.startsWith("MICROPHONE_") ||
|
|
55
|
+
e.code.startsWith("LIVEKIT_")
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
const devices = useDevices();
|
|
59
|
+
|
|
60
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
61
|
+
|
|
62
|
+
// Check if media controls are available and connected
|
|
63
|
+
const isConnected = connection.connected;
|
|
64
|
+
let mediaControls: MediaActions | null = null;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
if (sdk.livekit && isConnected) {
|
|
68
|
+
mediaControls = sdk.livekit.media;
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// Media controls not available - room not connected
|
|
72
|
+
mediaControls = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Enhanced wrapper functions with loading states and better error handling
|
|
76
|
+
const createEnhancedAction = (
|
|
77
|
+
action: () => Promise<void>,
|
|
78
|
+
actionName: string
|
|
79
|
+
) => {
|
|
80
|
+
return async (): Promise<void> => {
|
|
81
|
+
if (!mediaControls) {
|
|
82
|
+
const errorMsg = !isConnected
|
|
83
|
+
? "Cannot control media - not connected to LiveKit room"
|
|
84
|
+
: "Media controls not available - LiveKit service not initialized";
|
|
85
|
+
throw new Error(errorMsg);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setIsLoading(true);
|
|
89
|
+
try {
|
|
90
|
+
await action();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// Enhanced error handling with context
|
|
93
|
+
logger.error(`Failed to ${actionName}`, { actionName, error });
|
|
94
|
+
throw error;
|
|
95
|
+
} finally {
|
|
96
|
+
setIsLoading(false);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Fallback functions for when media controls are not available
|
|
102
|
+
const unavailableAction = async (): Promise<void> => {
|
|
103
|
+
const errorMsg = !isConnected
|
|
104
|
+
? "Cannot control media - not connected to LiveKit room"
|
|
105
|
+
: "Media controls not available - LiveKit service not initialized";
|
|
106
|
+
throw new Error(errorMsg);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Simple device switching functions without complex error handling
|
|
110
|
+
const switchCamera = async (deviceId: string): Promise<void> => {
|
|
111
|
+
try {
|
|
112
|
+
if (sdk.livekit?.devices) {
|
|
113
|
+
await sdk.livekit.devices.switchCamera(deviceId);
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
logger.error("Failed to switch camera", { error, deviceId });
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const switchMicrophone = async (deviceId: string): Promise<void> => {
|
|
122
|
+
try {
|
|
123
|
+
if (sdk.livekit?.devices) {
|
|
124
|
+
await sdk.livekit.devices.switchMicrophone(deviceId);
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
logger.error("Failed to switch microphone", { error, deviceId });
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const actions: EnhancedMediaActions = mediaControls
|
|
133
|
+
? {
|
|
134
|
+
enableCamera: createEnhancedAction(
|
|
135
|
+
() => mediaControls?.enableCamera(),
|
|
136
|
+
"enable camera"
|
|
137
|
+
),
|
|
138
|
+
disableCamera: createEnhancedAction(
|
|
139
|
+
() => mediaControls?.disableCamera(),
|
|
140
|
+
"disable camera"
|
|
141
|
+
),
|
|
142
|
+
enableMicrophone: createEnhancedAction(
|
|
143
|
+
() => mediaControls?.enableMicrophone(),
|
|
144
|
+
"enable microphone"
|
|
145
|
+
),
|
|
146
|
+
disableMicrophone: createEnhancedAction(
|
|
147
|
+
() => mediaControls?.disableMicrophone(),
|
|
148
|
+
"disable microphone"
|
|
149
|
+
),
|
|
150
|
+
toggleCamera: createEnhancedAction(
|
|
151
|
+
() => mediaControls?.toggleCamera(),
|
|
152
|
+
"toggle camera"
|
|
153
|
+
),
|
|
154
|
+
toggleMicrophone: createEnhancedAction(
|
|
155
|
+
() => mediaControls?.toggleMicrophone(),
|
|
156
|
+
"toggle microphone"
|
|
157
|
+
),
|
|
158
|
+
// Device switching
|
|
159
|
+
switchCamera,
|
|
160
|
+
switchMicrophone,
|
|
161
|
+
// Simple aliases
|
|
162
|
+
toggleAudio: createEnhancedAction(
|
|
163
|
+
() => mediaControls?.toggleMicrophone(),
|
|
164
|
+
"toggle audio"
|
|
165
|
+
),
|
|
166
|
+
toggleVideo: createEnhancedAction(
|
|
167
|
+
() => mediaControls?.toggleCamera(),
|
|
168
|
+
"toggle video"
|
|
169
|
+
),
|
|
170
|
+
}
|
|
171
|
+
: {
|
|
172
|
+
enableCamera: unavailableAction,
|
|
173
|
+
disableCamera: unavailableAction,
|
|
174
|
+
enableMicrophone: unavailableAction,
|
|
175
|
+
disableMicrophone: unavailableAction,
|
|
176
|
+
toggleCamera: unavailableAction,
|
|
177
|
+
toggleMicrophone: unavailableAction,
|
|
178
|
+
switchCamera: unavailableAction,
|
|
179
|
+
switchMicrophone: unavailableAction,
|
|
180
|
+
toggleAudio: unavailableAction,
|
|
181
|
+
toggleVideo: unavailableAction,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
// State
|
|
186
|
+
isVideoEnabled: local.videoEnabled,
|
|
187
|
+
isAudioEnabled: local.audioEnabled,
|
|
188
|
+
isCameraAvailable: !!mediaControls,
|
|
189
|
+
isMicrophoneAvailable: !!mediaControls,
|
|
190
|
+
isConnected,
|
|
191
|
+
isLoading,
|
|
192
|
+
errors,
|
|
193
|
+
|
|
194
|
+
// Device access
|
|
195
|
+
devices: {
|
|
196
|
+
cameras: devices.cams,
|
|
197
|
+
microphones: devices.mics,
|
|
198
|
+
speakers: devices.speakers,
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
// Enhanced Actions
|
|
202
|
+
enableCamera: actions.enableCamera,
|
|
203
|
+
disableCamera: actions.disableCamera,
|
|
204
|
+
enableMicrophone: actions.enableMicrophone,
|
|
205
|
+
disableMicrophone: actions.disableMicrophone,
|
|
206
|
+
toggleCamera: actions.toggleCamera,
|
|
207
|
+
toggleMicrophone: actions.toggleMicrophone,
|
|
208
|
+
// Device switching
|
|
209
|
+
switchCamera: actions.switchCamera,
|
|
210
|
+
switchMicrophone: actions.switchMicrophone,
|
|
211
|
+
// Simple aliases
|
|
212
|
+
toggleAudio: actions.toggleAudio,
|
|
213
|
+
toggleVideo: actions.toggleVideo,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { SdkEventType, eventBus } from "../core/events";
|
|
3
|
+
import { useRtcStore } from "../state/store";
|
|
4
|
+
import type { Participant } from "../state/types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Enhanced participant status interface following spec requirements
|
|
8
|
+
*/
|
|
9
|
+
export interface ParticipantStatus {
|
|
10
|
+
connectionState: "connecting" | "connected" | "reconnecting" | "disconnected";
|
|
11
|
+
mediaState: {
|
|
12
|
+
audio: "enabled" | "disabled" | "muted";
|
|
13
|
+
video: "enabled" | "disabled" | "camera_off";
|
|
14
|
+
};
|
|
15
|
+
networkQuality: "excellent" | "good" | "poor" | "lost" | "unknown";
|
|
16
|
+
lastSeen?: number;
|
|
17
|
+
speaking?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hook for tracking real-time participant status
|
|
22
|
+
*/
|
|
23
|
+
export function useParticipantStatus(participantId: string): ParticipantStatus {
|
|
24
|
+
const participant = useRtcStore(
|
|
25
|
+
(state) => state.room.participants[participantId]
|
|
26
|
+
);
|
|
27
|
+
const connection = useRtcStore((state) => state.connection);
|
|
28
|
+
|
|
29
|
+
const [status, setStatus] = useState<ParticipantStatus>(() =>
|
|
30
|
+
getInitialStatus(participant || null, connection.connected)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Update status when participant data changes
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!participant) {
|
|
36
|
+
setStatus(getDisconnectedStatus());
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
setStatus((prevStatus) => ({
|
|
41
|
+
...prevStatus,
|
|
42
|
+
connectionState: getConnectionState(participant, connection.connected),
|
|
43
|
+
mediaState: {
|
|
44
|
+
audio: getAudioState(participant),
|
|
45
|
+
video: getVideoState(participant),
|
|
46
|
+
},
|
|
47
|
+
networkQuality: participant.connectionQuality || "unknown",
|
|
48
|
+
lastSeen: participant.joinedAt || Date.now(),
|
|
49
|
+
speaking: participant.isSpeaking || false,
|
|
50
|
+
}));
|
|
51
|
+
}, [participant, connection.connected]);
|
|
52
|
+
|
|
53
|
+
// Listen for real-time media events
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const mediaEnabledSub = eventBus.on(SdkEventType.MEDIA_ENABLED, (event) => {
|
|
56
|
+
if (event.payload.participantId === participantId) {
|
|
57
|
+
setStatus((prevStatus) => ({
|
|
58
|
+
...prevStatus,
|
|
59
|
+
mediaState: {
|
|
60
|
+
...prevStatus.mediaState,
|
|
61
|
+
[event.payload.mediaType]: "enabled",
|
|
62
|
+
},
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const mediaDisabledSub = eventBus.on(
|
|
68
|
+
SdkEventType.MEDIA_DISABLED,
|
|
69
|
+
(event) => {
|
|
70
|
+
if (event.payload.participantId === participantId) {
|
|
71
|
+
setStatus((prevStatus) => ({
|
|
72
|
+
...prevStatus,
|
|
73
|
+
mediaState: {
|
|
74
|
+
...prevStatus.mediaState,
|
|
75
|
+
[event.payload.mediaType]: "disabled",
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const connectionQualitySub = eventBus.on(
|
|
83
|
+
SdkEventType.CONNECTION_QUALITY_CHANGED,
|
|
84
|
+
(event) => {
|
|
85
|
+
if (event.payload.participantId === participantId) {
|
|
86
|
+
setStatus((prevStatus) => ({
|
|
87
|
+
...prevStatus,
|
|
88
|
+
networkQuality: event.payload.quality,
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const participantJoinedSub = eventBus.on(
|
|
95
|
+
SdkEventType.PARTICIPANT_JOINED,
|
|
96
|
+
(event) => {
|
|
97
|
+
if (event.payload.participant.id === participantId) {
|
|
98
|
+
setStatus((prevStatus) => ({
|
|
99
|
+
...prevStatus,
|
|
100
|
+
connectionState: "connected",
|
|
101
|
+
lastSeen: event.timestamp,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const participantLeftSub = eventBus.on(
|
|
108
|
+
SdkEventType.PARTICIPANT_LEFT,
|
|
109
|
+
(event) => {
|
|
110
|
+
if (event.payload.participantId === participantId) {
|
|
111
|
+
setStatus((prevStatus) => ({
|
|
112
|
+
...prevStatus,
|
|
113
|
+
connectionState: "disconnected",
|
|
114
|
+
lastSeen: event.timestamp,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return () => {
|
|
121
|
+
mediaEnabledSub.unsubscribe();
|
|
122
|
+
mediaDisabledSub.unsubscribe();
|
|
123
|
+
connectionQualitySub.unsubscribe();
|
|
124
|
+
participantJoinedSub.unsubscribe();
|
|
125
|
+
participantLeftSub.unsubscribe();
|
|
126
|
+
};
|
|
127
|
+
}, [participantId]);
|
|
128
|
+
|
|
129
|
+
return status;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Hook for tracking multiple participants' status
|
|
134
|
+
*/
|
|
135
|
+
export function useParticipantsStatus(
|
|
136
|
+
participantIds: string[]
|
|
137
|
+
): Record<string, ParticipantStatus> {
|
|
138
|
+
const [statuses, setStatuses] = useState<Record<string, ParticipantStatus>>(
|
|
139
|
+
{}
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const updateStatus = (id: string, status: ParticipantStatus) => {
|
|
144
|
+
setStatuses((prev) => ({
|
|
145
|
+
...prev,
|
|
146
|
+
[id]: status,
|
|
147
|
+
}));
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Initialize statuses
|
|
151
|
+
const initialStatuses: Record<string, ParticipantStatus> = {};
|
|
152
|
+
for (const id of participantIds) {
|
|
153
|
+
initialStatuses[id] = getInitialStatus(null, false);
|
|
154
|
+
}
|
|
155
|
+
setStatuses(initialStatuses);
|
|
156
|
+
|
|
157
|
+
// Set up event listeners for all participants
|
|
158
|
+
const subscriptions = [
|
|
159
|
+
eventBus.onPattern("*", (event) => {
|
|
160
|
+
const participantId =
|
|
161
|
+
event.payload?.participantId || event.payload?.participant?.id;
|
|
162
|
+
if (participantId && participantIds.includes(participantId)) {
|
|
163
|
+
// Update the specific participant's status
|
|
164
|
+
setStatuses((prev) => {
|
|
165
|
+
const currentStatus =
|
|
166
|
+
prev[participantId] || getInitialStatus(null, false);
|
|
167
|
+
return {
|
|
168
|
+
...prev,
|
|
169
|
+
[participantId]: updateStatusFromEvent(currentStatus, event),
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}),
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
return () => {
|
|
177
|
+
for (const sub of subscriptions) {
|
|
178
|
+
sub.unsubscribe();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}, [participantIds]);
|
|
182
|
+
|
|
183
|
+
return statuses;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Hook for getting all participants with their real-time status
|
|
188
|
+
*/
|
|
189
|
+
export function useParticipantsWithStatus(): (Participant & {
|
|
190
|
+
status: ParticipantStatus;
|
|
191
|
+
})[] {
|
|
192
|
+
const participants = useRtcStore((state) =>
|
|
193
|
+
Object.values(state.room.participants)
|
|
194
|
+
);
|
|
195
|
+
const connection = useRtcStore((state) => state.connection);
|
|
196
|
+
|
|
197
|
+
return participants.map((participant) => ({
|
|
198
|
+
...participant,
|
|
199
|
+
status: {
|
|
200
|
+
connectionState: getConnectionState(participant, connection.connected),
|
|
201
|
+
mediaState: {
|
|
202
|
+
audio: getAudioState(participant),
|
|
203
|
+
video: getVideoState(participant),
|
|
204
|
+
},
|
|
205
|
+
networkQuality: participant.connectionQuality || "unknown",
|
|
206
|
+
lastSeen: participant.joinedAt || Date.now(),
|
|
207
|
+
speaking: participant.isSpeaking || false,
|
|
208
|
+
},
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getInitialStatus(
|
|
213
|
+
participant: Participant | null,
|
|
214
|
+
isConnected: boolean
|
|
215
|
+
): ParticipantStatus {
|
|
216
|
+
if (!participant) {
|
|
217
|
+
return getDisconnectedStatus();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
connectionState: getConnectionState(participant, isConnected),
|
|
222
|
+
mediaState: {
|
|
223
|
+
audio: getAudioState(participant),
|
|
224
|
+
video: getVideoState(participant),
|
|
225
|
+
},
|
|
226
|
+
networkQuality: participant.connectionQuality || "unknown",
|
|
227
|
+
lastSeen: participant.joinedAt || Date.now(),
|
|
228
|
+
speaking: participant.isSpeaking || false,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getDisconnectedStatus(): ParticipantStatus {
|
|
233
|
+
return {
|
|
234
|
+
connectionState: "disconnected",
|
|
235
|
+
mediaState: {
|
|
236
|
+
audio: "disabled",
|
|
237
|
+
video: "disabled",
|
|
238
|
+
},
|
|
239
|
+
networkQuality: "unknown",
|
|
240
|
+
speaking: false,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getConnectionState(
|
|
245
|
+
participant: Participant,
|
|
246
|
+
isGloballyConnected: boolean
|
|
247
|
+
): ParticipantStatus["connectionState"] {
|
|
248
|
+
if (!isGloballyConnected) {
|
|
249
|
+
return "disconnected";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
switch (participant.callState) {
|
|
253
|
+
case "JOINED":
|
|
254
|
+
return "connected";
|
|
255
|
+
case "RINGING":
|
|
256
|
+
case "INVITED":
|
|
257
|
+
return "connecting";
|
|
258
|
+
case "LEFT":
|
|
259
|
+
return "disconnected";
|
|
260
|
+
default:
|
|
261
|
+
return "disconnected";
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function getAudioState(
|
|
266
|
+
participant: Participant
|
|
267
|
+
): ParticipantStatus["mediaState"]["audio"] {
|
|
268
|
+
if (!participant.audioEnabled) {
|
|
269
|
+
return "disabled";
|
|
270
|
+
}
|
|
271
|
+
// Could add muted state detection here based on additional data
|
|
272
|
+
return "enabled";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function getVideoState(
|
|
276
|
+
participant: Participant
|
|
277
|
+
): ParticipantStatus["mediaState"]["video"] {
|
|
278
|
+
if (!participant.videoEnabled) {
|
|
279
|
+
return "camera_off";
|
|
280
|
+
}
|
|
281
|
+
return "enabled";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function updateStatusFromEvent(
|
|
285
|
+
currentStatus: ParticipantStatus,
|
|
286
|
+
event: any
|
|
287
|
+
): ParticipantStatus {
|
|
288
|
+
const newStatus = { ...currentStatus };
|
|
289
|
+
|
|
290
|
+
switch (event.type) {
|
|
291
|
+
case SdkEventType.MEDIA_ENABLED:
|
|
292
|
+
newStatus.mediaState = {
|
|
293
|
+
...newStatus.mediaState,
|
|
294
|
+
[event.payload.mediaType]: "enabled",
|
|
295
|
+
};
|
|
296
|
+
break;
|
|
297
|
+
case SdkEventType.MEDIA_DISABLED:
|
|
298
|
+
newStatus.mediaState = {
|
|
299
|
+
...newStatus.mediaState,
|
|
300
|
+
[event.payload.mediaType]:
|
|
301
|
+
event.payload.mediaType === "video" ? "camera_off" : "disabled",
|
|
302
|
+
};
|
|
303
|
+
break;
|
|
304
|
+
case SdkEventType.CONNECTION_QUALITY_CHANGED:
|
|
305
|
+
newStatus.networkQuality = event.payload.quality;
|
|
306
|
+
break;
|
|
307
|
+
case SdkEventType.PARTICIPANT_JOINED:
|
|
308
|
+
newStatus.connectionState = "connected";
|
|
309
|
+
newStatus.lastSeen = event.timestamp;
|
|
310
|
+
break;
|
|
311
|
+
case SdkEventType.PARTICIPANT_LEFT:
|
|
312
|
+
newStatus.connectionState = "disconnected";
|
|
313
|
+
newStatus.lastSeen = event.timestamp;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return newStatus;
|
|
318
|
+
}
|