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,371 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConnectionQuality,
|
|
3
|
+
LocalParticipant,
|
|
4
|
+
type Participant,
|
|
5
|
+
type Room,
|
|
6
|
+
RoomEvent,
|
|
7
|
+
Track,
|
|
8
|
+
type TrackPublication,
|
|
9
|
+
} from "livekit-client";
|
|
10
|
+
import { SdkEventType, eventBus } from "../../core/events";
|
|
11
|
+
import { rtcStore } from "../../state/store";
|
|
12
|
+
import { trackRegistry } from "./trackRegistry";
|
|
13
|
+
|
|
14
|
+
export interface EventBridgeOptions {
|
|
15
|
+
log?: (
|
|
16
|
+
lvl: "debug" | "info" | "warn" | "error",
|
|
17
|
+
msg: string,
|
|
18
|
+
extra?: any
|
|
19
|
+
) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class LiveKitEventBridge {
|
|
23
|
+
private room: Room;
|
|
24
|
+
private opts: EventBridgeOptions;
|
|
25
|
+
|
|
26
|
+
constructor(room: Room, opts: EventBridgeOptions = {}) {
|
|
27
|
+
this.room = room;
|
|
28
|
+
this.opts = opts;
|
|
29
|
+
this.setupEventListeners();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private setupEventListeners(): void {
|
|
33
|
+
this.room
|
|
34
|
+
// Connection events
|
|
35
|
+
.on(RoomEvent.Connected, this.handleConnected)
|
|
36
|
+
.on(RoomEvent.Disconnected, this.handleDisconnected)
|
|
37
|
+
.on(RoomEvent.Reconnecting, this.handleReconnecting)
|
|
38
|
+
.on(RoomEvent.Reconnected, this.handleReconnected)
|
|
39
|
+
|
|
40
|
+
// Participant events
|
|
41
|
+
.on(RoomEvent.ParticipantConnected, this.handleParticipantConnected)
|
|
42
|
+
.on(RoomEvent.ParticipantDisconnected, this.handleParticipantDisconnected)
|
|
43
|
+
|
|
44
|
+
// Track events
|
|
45
|
+
.on(RoomEvent.TrackSubscribed, this.handleTrackSubscribed)
|
|
46
|
+
.on(RoomEvent.TrackUnsubscribed, this.handleTrackUnsubscribed)
|
|
47
|
+
.on(RoomEvent.TrackMuted, this.handleTrackMuted)
|
|
48
|
+
.on(RoomEvent.TrackUnmuted, this.handleTrackUnmuted)
|
|
49
|
+
|
|
50
|
+
// Media events
|
|
51
|
+
.on(RoomEvent.ActiveSpeakersChanged, this.handleActiveSpeakersChanged)
|
|
52
|
+
.on(
|
|
53
|
+
RoomEvent.ConnectionQualityChanged,
|
|
54
|
+
this.handleConnectionQualityChanged
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private handleConnected = (): void => {
|
|
59
|
+
this.opts.log?.("info", "LiveKit room connected");
|
|
60
|
+
|
|
61
|
+
rtcStore.getState().patch((state) => {
|
|
62
|
+
state.connection.connected = true;
|
|
63
|
+
state.connection.reconnecting = false;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Sync all existing participants (including local)
|
|
67
|
+
this.syncAllParticipants();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
private handleDisconnected = (): void => {
|
|
71
|
+
this.opts.log?.("info", "LiveKit room disconnected");
|
|
72
|
+
|
|
73
|
+
rtcStore.getState().patch((state) => {
|
|
74
|
+
state.connection.connected = false;
|
|
75
|
+
state.connection.reconnecting = false;
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
private handleReconnecting = (): void => {
|
|
80
|
+
this.opts.log?.("info", "LiveKit room reconnecting");
|
|
81
|
+
|
|
82
|
+
rtcStore.getState().patch((state) => {
|
|
83
|
+
state.connection.reconnecting = true;
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
private handleReconnected = (): void => {
|
|
88
|
+
this.opts.log?.("info", "LiveKit room reconnected");
|
|
89
|
+
|
|
90
|
+
rtcStore.getState().patch((state) => {
|
|
91
|
+
state.connection.connected = true;
|
|
92
|
+
state.connection.reconnecting = false;
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
private handleParticipantConnected = (participant: Participant): void => {
|
|
97
|
+
const pid = participant.identity;
|
|
98
|
+
this.opts.log?.("info", "Participant connected", { pid });
|
|
99
|
+
|
|
100
|
+
rtcStore.getState().patch((state) => {
|
|
101
|
+
// Create or update participant in unified state
|
|
102
|
+
if (!state.room.participants[pid]) {
|
|
103
|
+
state.room.participants[pid] = {
|
|
104
|
+
id: pid,
|
|
105
|
+
firstName: participant.name || "Unknown",
|
|
106
|
+
role: "MEMBER",
|
|
107
|
+
callState: "JOINED",
|
|
108
|
+
joinedAt: Date.now(),
|
|
109
|
+
audioEnabled: true,
|
|
110
|
+
videoEnabled: true,
|
|
111
|
+
isSpeaking: false,
|
|
112
|
+
};
|
|
113
|
+
this.opts.log?.("debug", "Created participant", { pid });
|
|
114
|
+
} else {
|
|
115
|
+
// Update existing participant
|
|
116
|
+
state.room.participants[pid].callState = "JOINED";
|
|
117
|
+
if (!state.room.participants[pid].joinedAt) {
|
|
118
|
+
state.room.participants[pid].joinedAt = Date.now();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
private handleParticipantDisconnected = (participant: Participant): void => {
|
|
125
|
+
const pid = participant.identity;
|
|
126
|
+
this.opts.log?.("info", "Participant disconnected", { pid });
|
|
127
|
+
|
|
128
|
+
// Simple strategy: don't immediately mark as left, rely on call.ended
|
|
129
|
+
// This handles transient disconnects gracefully
|
|
130
|
+
|
|
131
|
+
rtcStore.getState().patch((state) => {
|
|
132
|
+
// Clean up tracks for this participant
|
|
133
|
+
trackRegistry.removeByParticipant(pid);
|
|
134
|
+
|
|
135
|
+
// Keep profile and presence for history - UI can filter as needed
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
private handleTrackSubscribed = (
|
|
140
|
+
track: Track,
|
|
141
|
+
publication: TrackPublication,
|
|
142
|
+
participant: Participant
|
|
143
|
+
): void => {
|
|
144
|
+
const pid = participant.identity;
|
|
145
|
+
const trackSid = publication.trackSid;
|
|
146
|
+
|
|
147
|
+
this.opts.log?.("debug", "Track subscribed", {
|
|
148
|
+
pid,
|
|
149
|
+
trackSid,
|
|
150
|
+
kind: track.kind,
|
|
151
|
+
source: publication.source,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Add to track registry
|
|
155
|
+
trackRegistry.add(trackSid, pid, track.kind, publication.source);
|
|
156
|
+
|
|
157
|
+
// Update participant mute states (for backward compatibility)
|
|
158
|
+
this.updateParticipantMuteState(participant);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
private handleTrackUnsubscribed = (
|
|
162
|
+
track: Track,
|
|
163
|
+
publication: TrackPublication,
|
|
164
|
+
participant: Participant
|
|
165
|
+
): void => {
|
|
166
|
+
const pid = participant.identity;
|
|
167
|
+
const trackSid = publication.trackSid;
|
|
168
|
+
|
|
169
|
+
this.opts.log?.("debug", "Track unsubscribed", {
|
|
170
|
+
pid,
|
|
171
|
+
trackSid,
|
|
172
|
+
kind: track.kind,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Remove from track registry
|
|
176
|
+
trackRegistry.remove(trackSid);
|
|
177
|
+
|
|
178
|
+
// Update participant mute states (for backward compatibility)
|
|
179
|
+
this.updateParticipantMuteState(participant);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
private handleTrackMuted = (
|
|
183
|
+
publication: TrackPublication,
|
|
184
|
+
participant: Participant
|
|
185
|
+
): void => {
|
|
186
|
+
const pid = participant.identity;
|
|
187
|
+
const trackSid = publication.trackSid;
|
|
188
|
+
|
|
189
|
+
this.opts.log?.("debug", "Track muted", {
|
|
190
|
+
pid,
|
|
191
|
+
trackSid,
|
|
192
|
+
kind: publication.kind,
|
|
193
|
+
source: publication.source,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Update participant mute states
|
|
197
|
+
this.updateParticipantMuteState(participant);
|
|
198
|
+
|
|
199
|
+
// Update local state if it's our own track
|
|
200
|
+
if (participant.isLocal) {
|
|
201
|
+
rtcStore.getState().patch((state) => {
|
|
202
|
+
if (publication.source === Track.Source.Microphone) {
|
|
203
|
+
state.local.audioEnabled = false;
|
|
204
|
+
} else if (publication.source === Track.Source.Camera) {
|
|
205
|
+
state.local.videoEnabled = false;
|
|
206
|
+
} else if (publication.source === Track.Source.ScreenShare) {
|
|
207
|
+
state.local.screenEnabled = false;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
private handleTrackUnmuted = (
|
|
214
|
+
publication: TrackPublication,
|
|
215
|
+
participant: Participant
|
|
216
|
+
): void => {
|
|
217
|
+
const pid = participant.identity;
|
|
218
|
+
const trackSid = publication.trackSid;
|
|
219
|
+
|
|
220
|
+
this.opts.log?.("debug", "Track unmuted", {
|
|
221
|
+
pid,
|
|
222
|
+
trackSid,
|
|
223
|
+
kind: publication.kind,
|
|
224
|
+
source: publication.source,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Update participant mute states
|
|
228
|
+
this.updateParticipantMuteState(participant);
|
|
229
|
+
|
|
230
|
+
// Update local state if it's our own track
|
|
231
|
+
if (participant.isLocal) {
|
|
232
|
+
rtcStore.getState().patch((state) => {
|
|
233
|
+
if (publication.source === Track.Source.Microphone) {
|
|
234
|
+
state.local.audioEnabled = true;
|
|
235
|
+
} else if (publication.source === Track.Source.Camera) {
|
|
236
|
+
state.local.videoEnabled = true;
|
|
237
|
+
} else if (publication.source === Track.Source.ScreenShare) {
|
|
238
|
+
state.local.screenEnabled = true;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
private handleActiveSpeakersChanged = (speakers: Participant[]): void => {
|
|
245
|
+
const speakerIds = new Set(speakers.map((s) => s.identity));
|
|
246
|
+
|
|
247
|
+
this.opts.log?.("debug", "Active speakers changed", {
|
|
248
|
+
speakers: Array.from(speakerIds),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
rtcStore.getState().patch((state) => {
|
|
252
|
+
// Update speaking state for all participants
|
|
253
|
+
for (const [pid, participant] of Object.entries(
|
|
254
|
+
state.room.participants
|
|
255
|
+
)) {
|
|
256
|
+
participant.isSpeaking = speakerIds.has(pid);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
private handleConnectionQualityChanged = (
|
|
262
|
+
quality: ConnectionQuality,
|
|
263
|
+
participant: Participant
|
|
264
|
+
): void => {
|
|
265
|
+
const pid = participant.identity;
|
|
266
|
+
const qualityLabel = this.mapConnectionQuality(quality);
|
|
267
|
+
|
|
268
|
+
this.opts.log?.("debug", "Connection quality changed", {
|
|
269
|
+
pid,
|
|
270
|
+
quality: qualityLabel,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
rtcStore.getState().patch((state) => {
|
|
274
|
+
// Update participant connection quality in unified state
|
|
275
|
+
if (state.room.participants[pid]) {
|
|
276
|
+
state.room.participants[pid].connectionQuality = qualityLabel;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Update local connection quality if it's the local participant
|
|
280
|
+
if (participant.isLocal) {
|
|
281
|
+
state.connection.quality = qualityLabel;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Emit SDK event for connection quality change
|
|
286
|
+
eventBus.emit(
|
|
287
|
+
SdkEventType.CONNECTION_QUALITY_CHANGED,
|
|
288
|
+
{
|
|
289
|
+
participantId: pid,
|
|
290
|
+
quality: qualityLabel,
|
|
291
|
+
timestamp: Date.now(),
|
|
292
|
+
},
|
|
293
|
+
"livekit"
|
|
294
|
+
);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
private syncAllParticipants(): void {
|
|
298
|
+
const allParticipants = [
|
|
299
|
+
this.room.localParticipant,
|
|
300
|
+
...Array.from(this.room.remoteParticipants.values()),
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
for (const participant of allParticipants) {
|
|
304
|
+
this.handleParticipantConnected(participant);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private updateParticipantMuteState(participant: Participant): void {
|
|
309
|
+
// Media mute state will be tracked via track subscription/unsubscription events
|
|
310
|
+
// No need to maintain separate mute state in the new architecture
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private getAudioMutedState(participant: Participant): boolean {
|
|
314
|
+
const audioPublication = participant.getTrackPublication(
|
|
315
|
+
Track.Source.Microphone
|
|
316
|
+
);
|
|
317
|
+
return (
|
|
318
|
+
!audioPublication ||
|
|
319
|
+
audioPublication.isMuted ||
|
|
320
|
+
!audioPublication.isEnabled
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private getVideoMutedState(participant: Participant): boolean {
|
|
325
|
+
const videoPublication = participant.getTrackPublication(
|
|
326
|
+
Track.Source.Camera
|
|
327
|
+
);
|
|
328
|
+
return (
|
|
329
|
+
!videoPublication ||
|
|
330
|
+
videoPublication.isMuted ||
|
|
331
|
+
!videoPublication.isEnabled
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private mapConnectionQuality(
|
|
336
|
+
quality: ConnectionQuality
|
|
337
|
+
): "excellent" | "good" | "poor" | "lost" {
|
|
338
|
+
switch (quality) {
|
|
339
|
+
case ConnectionQuality.Excellent:
|
|
340
|
+
return "excellent";
|
|
341
|
+
case ConnectionQuality.Good:
|
|
342
|
+
return "good";
|
|
343
|
+
case ConnectionQuality.Poor:
|
|
344
|
+
return "poor";
|
|
345
|
+
default:
|
|
346
|
+
return "lost";
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
destroy(): void {
|
|
351
|
+
this.room
|
|
352
|
+
.off(RoomEvent.Connected, this.handleConnected)
|
|
353
|
+
.off(RoomEvent.Disconnected, this.handleDisconnected)
|
|
354
|
+
.off(RoomEvent.Reconnecting, this.handleReconnecting)
|
|
355
|
+
.off(RoomEvent.Reconnected, this.handleReconnected)
|
|
356
|
+
.off(RoomEvent.ParticipantConnected, this.handleParticipantConnected)
|
|
357
|
+
.off(
|
|
358
|
+
RoomEvent.ParticipantDisconnected,
|
|
359
|
+
this.handleParticipantDisconnected
|
|
360
|
+
)
|
|
361
|
+
.off(RoomEvent.TrackSubscribed, this.handleTrackSubscribed)
|
|
362
|
+
.off(RoomEvent.TrackUnsubscribed, this.handleTrackUnsubscribed)
|
|
363
|
+
.off(RoomEvent.TrackMuted, this.handleTrackMuted)
|
|
364
|
+
.off(RoomEvent.TrackUnmuted, this.handleTrackUnmuted)
|
|
365
|
+
.off(RoomEvent.ActiveSpeakersChanged, this.handleActiveSpeakersChanged)
|
|
366
|
+
.off(
|
|
367
|
+
RoomEvent.ConnectionQualityChanged,
|
|
368
|
+
this.handleConnectionQualityChanged
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Track } from "livekit-client";
|
|
2
|
+
|
|
3
|
+
interface TrackState {
|
|
4
|
+
sid: string;
|
|
5
|
+
participantId: string;
|
|
6
|
+
kind: "audio" | "video" | "screen";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class TrackRegistry {
|
|
10
|
+
private tracks: Record<string, TrackState> = {};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Add or update a track in the registry
|
|
14
|
+
* Idempotent: safe to call multiple times with same params
|
|
15
|
+
*/
|
|
16
|
+
add(
|
|
17
|
+
sid: string,
|
|
18
|
+
participantId: string,
|
|
19
|
+
kind: Track.Kind,
|
|
20
|
+
source?: Track.Source
|
|
21
|
+
): void {
|
|
22
|
+
this.tracks[sid] = {
|
|
23
|
+
sid,
|
|
24
|
+
participantId,
|
|
25
|
+
kind: source ? this.mapTrackSource(source) : this.mapTrackKind(kind),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Remove a track from the registry
|
|
31
|
+
* Idempotent: ignores unknown sids
|
|
32
|
+
*/
|
|
33
|
+
remove(sid: string): void {
|
|
34
|
+
delete this.tracks[sid];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Remove all tracks for a specific participant
|
|
39
|
+
* Used when participant disconnects
|
|
40
|
+
*/
|
|
41
|
+
removeByParticipant(participantId: string): void {
|
|
42
|
+
const tracksToRemove: string[] = [];
|
|
43
|
+
|
|
44
|
+
for (const [sid, track] of Object.entries(this.tracks)) {
|
|
45
|
+
if (track.participantId === participantId) {
|
|
46
|
+
tracksToRemove.push(sid);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const sid of tracksToRemove) {
|
|
51
|
+
delete this.tracks[sid];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get all tracks for a specific participant
|
|
57
|
+
*/
|
|
58
|
+
getByParticipant(participantId: string): TrackState[] {
|
|
59
|
+
return Object.values(this.tracks).filter(
|
|
60
|
+
(track) => track.participantId === participantId
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get all tracks
|
|
66
|
+
*/
|
|
67
|
+
getAll(): TrackState[] {
|
|
68
|
+
return Object.values(this.tracks);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Clear all tracks
|
|
73
|
+
*/
|
|
74
|
+
clear(): void {
|
|
75
|
+
this.tracks = {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Map LiveKit Track.Kind to our TrackState kind
|
|
80
|
+
* Note: This is deprecated - use mapTrackSource for better accuracy
|
|
81
|
+
*/
|
|
82
|
+
private mapTrackKind(kind: Track.Kind): "audio" | "video" | "screen" {
|
|
83
|
+
switch (kind) {
|
|
84
|
+
case Track.Kind.Audio:
|
|
85
|
+
return "audio";
|
|
86
|
+
case Track.Kind.Video:
|
|
87
|
+
return "video";
|
|
88
|
+
default:
|
|
89
|
+
// Fallback for unknown kinds
|
|
90
|
+
return "video";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Map LiveKit Track.Source to our TrackState kind (more accurate)
|
|
96
|
+
*/
|
|
97
|
+
private mapTrackSource(source: Track.Source): "audio" | "video" | "screen" {
|
|
98
|
+
switch (source) {
|
|
99
|
+
case Track.Source.Microphone:
|
|
100
|
+
return "audio";
|
|
101
|
+
case Track.Source.Camera:
|
|
102
|
+
return "video";
|
|
103
|
+
case Track.Source.ScreenShare:
|
|
104
|
+
case Track.Source.ScreenShareAudio:
|
|
105
|
+
return "screen";
|
|
106
|
+
default:
|
|
107
|
+
// Fallback to generic video for unknown sources
|
|
108
|
+
return "video";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Singleton instance
|
|
114
|
+
export const trackRegistry = new TrackRegistry();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Internal LiveKit exports for SDK
|
|
2
|
+
export * from "./types";
|
|
3
|
+
export * from "./constants";
|
|
4
|
+
export { LiveKitService } from "./livekit.service";
|
|
5
|
+
export { LiveKitEventBridge } from "./events/eventBridge";
|
|
6
|
+
export { trackRegistry } from "./events/trackRegistry";
|
|
7
|
+
export type { MediaErrorInfo } from "./error-classifier";
|
|
8
|
+
export {
|
|
9
|
+
classifyMediaError,
|
|
10
|
+
MediaDeviceError,
|
|
11
|
+
MediaPermissionError,
|
|
12
|
+
MediaNotFoundError,
|
|
13
|
+
MediaInUseError,
|
|
14
|
+
MediaUnknownError,
|
|
15
|
+
} from "./error-classifier";
|
|
16
|
+
|
|
17
|
+
// Additional exports for subpath consumers
|
|
18
|
+
export { DeviceManager } from "./device.manager";
|
|
19
|
+
export { MediaControls } from "./media.controls";
|
|
20
|
+
export { RoomManager } from "./room.manager";
|
|
21
|
+
|
|
22
|
+
// Track utilities
|
|
23
|
+
export * from "./track.utils";
|
|
24
|
+
|
|
25
|
+
// Re-export essential LiveKit client types that consumers might need
|
|
26
|
+
export type {
|
|
27
|
+
Room,
|
|
28
|
+
RoomOptions,
|
|
29
|
+
Participant,
|
|
30
|
+
LocalParticipant,
|
|
31
|
+
RemoteParticipant,
|
|
32
|
+
Track,
|
|
33
|
+
LocalTrack,
|
|
34
|
+
RemoteTrack,
|
|
35
|
+
AudioTrack,
|
|
36
|
+
VideoTrack,
|
|
37
|
+
TrackPublication,
|
|
38
|
+
LocalTrackPublication,
|
|
39
|
+
RemoteTrackPublication,
|
|
40
|
+
ConnectionQuality,
|
|
41
|
+
ConnectionState,
|
|
42
|
+
ParticipantEvent,
|
|
43
|
+
RoomEvent,
|
|
44
|
+
TrackEvent,
|
|
45
|
+
// Additional utility types
|
|
46
|
+
ReconnectPolicy,
|
|
47
|
+
// Newer types that might be useful
|
|
48
|
+
DataPacket_Kind,
|
|
49
|
+
} from "livekit-client";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Room, RoomOptions } from "livekit-client";
|
|
2
|
+
import { DeviceManager } from "./device.manager";
|
|
3
|
+
import { LiveKitEventBridge } from "./events/eventBridge";
|
|
4
|
+
import { MediaControls } from "./media.controls";
|
|
5
|
+
import { RoomManager } from "./room.manager";
|
|
6
|
+
import type { LiveKitServiceOptions } from "./types";
|
|
7
|
+
|
|
8
|
+
export class LiveKitService {
|
|
9
|
+
private roomManager: RoomManager;
|
|
10
|
+
private eventBridge: LiveKitEventBridge | undefined;
|
|
11
|
+
private mediaControls: MediaControls | undefined;
|
|
12
|
+
private deviceManager: DeviceManager | undefined;
|
|
13
|
+
private options: LiveKitServiceOptions;
|
|
14
|
+
|
|
15
|
+
constructor(options: LiveKitServiceOptions = { log: undefined }) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
this.roomManager = new RoomManager();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async joinRoom(token: string, url: string): Promise<void> {
|
|
21
|
+
if (!url) {
|
|
22
|
+
const error = new Error("LiveKit URL is required");
|
|
23
|
+
this.options.log?.("error", "LiveKit URL missing", { token, url });
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
this.options.log?.("info", "Joining LiveKit room", { url });
|
|
29
|
+
await this.roomManager.connect({ url, token });
|
|
30
|
+
|
|
31
|
+
// Initialize managers after a successful connection
|
|
32
|
+
const eventBridgeOptions: {
|
|
33
|
+
log?: (
|
|
34
|
+
lvl: "debug" | "info" | "warn" | "error",
|
|
35
|
+
msg: string,
|
|
36
|
+
extra?: any
|
|
37
|
+
) => void;
|
|
38
|
+
} = {};
|
|
39
|
+
if (this.options.log) {
|
|
40
|
+
eventBridgeOptions.log = this.options.log;
|
|
41
|
+
}
|
|
42
|
+
this.eventBridge = new LiveKitEventBridge(this.room, eventBridgeOptions);
|
|
43
|
+
this.mediaControls = new MediaControls(
|
|
44
|
+
this.room.localParticipant,
|
|
45
|
+
this.room
|
|
46
|
+
);
|
|
47
|
+
this.deviceManager = new DeviceManager(this.room);
|
|
48
|
+
|
|
49
|
+
// Enumerate devices after connection
|
|
50
|
+
try {
|
|
51
|
+
await this.deviceManager.enumerateDevices();
|
|
52
|
+
this.options.log?.("info", "Device enumeration completed");
|
|
53
|
+
} catch (error) {
|
|
54
|
+
this.options.log?.("warn", "Failed to enumerate devices", error);
|
|
55
|
+
// Don't fail the connection if device enumeration fails
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.options.log?.("info", "Successfully joined LiveKit room");
|
|
59
|
+
} catch (error) {
|
|
60
|
+
this.options.log?.("error", "Failed to join LiveKit room", error);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async disconnect(): Promise<void> {
|
|
66
|
+
try {
|
|
67
|
+
this.options.log?.("info", "Disconnecting from LiveKit room");
|
|
68
|
+
|
|
69
|
+
// Cleanup managers first
|
|
70
|
+
this.eventBridge?.destroy();
|
|
71
|
+
this.eventBridge = undefined;
|
|
72
|
+
this.deviceManager?.destroy();
|
|
73
|
+
this.deviceManager = undefined;
|
|
74
|
+
this.mediaControls = undefined;
|
|
75
|
+
|
|
76
|
+
await this.roomManager.disconnect();
|
|
77
|
+
this.options.log?.("info", "Successfully disconnected from LiveKit room");
|
|
78
|
+
} catch (error) {
|
|
79
|
+
this.options.log?.("error", "Error during LiveKit disconnect", error);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get room(): Room {
|
|
85
|
+
return this.roomManager.room;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get media(): MediaControls {
|
|
89
|
+
if (!this.mediaControls) {
|
|
90
|
+
throw new Error("Media controls not available - room not connected");
|
|
91
|
+
}
|
|
92
|
+
return this.mediaControls;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get devices(): DeviceManager {
|
|
96
|
+
if (!this.deviceManager) {
|
|
97
|
+
throw new Error("Device manager not available - room not connected");
|
|
98
|
+
}
|
|
99
|
+
return this.deviceManager;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
destroy(): void {
|
|
103
|
+
this.eventBridge?.destroy();
|
|
104
|
+
this.eventBridge = undefined;
|
|
105
|
+
this.deviceManager?.destroy();
|
|
106
|
+
this.deviceManager = undefined;
|
|
107
|
+
this.mediaControls = undefined;
|
|
108
|
+
this.roomManager.destroy();
|
|
109
|
+
}
|
|
110
|
+
}
|