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,416 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useSdk } from "../provider/RtcProvider";
|
|
3
|
+
import { useRtcStore } from "../state/store";
|
|
4
|
+
import { createLogger } from "../utils/logger";
|
|
5
|
+
|
|
6
|
+
const logger = createLogger("hooks:call-quality");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Call quality metrics interface following spec requirements
|
|
10
|
+
*/
|
|
11
|
+
export interface CallQuality {
|
|
12
|
+
overall: "excellent" | "good" | "poor" | "failed";
|
|
13
|
+
metrics: {
|
|
14
|
+
latency: number; // ms
|
|
15
|
+
packetLoss: number; // percentage
|
|
16
|
+
bandwidth: {
|
|
17
|
+
upload: number; // kbps
|
|
18
|
+
download: number; // kbps
|
|
19
|
+
};
|
|
20
|
+
resolution?: {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
timestamp: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Hook for monitoring call quality metrics
|
|
30
|
+
*/
|
|
31
|
+
export function useCallQuality(): {
|
|
32
|
+
quality: CallQuality | null;
|
|
33
|
+
history: CallQuality[];
|
|
34
|
+
} {
|
|
35
|
+
const sdk = useSdk();
|
|
36
|
+
const isConnected = useRtcStore((state) => state.connection.connected);
|
|
37
|
+
const [quality, setQuality] = useState<CallQuality | null>(null);
|
|
38
|
+
const [history, setHistory] = useState<CallQuality[]>([]);
|
|
39
|
+
const intervalRef = useRef<number | null>(null);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!isConnected || !sdk.livekit?.room) {
|
|
43
|
+
// Clear quality when disconnected
|
|
44
|
+
setQuality(null);
|
|
45
|
+
if (intervalRef.current) {
|
|
46
|
+
clearInterval(intervalRef.current);
|
|
47
|
+
}
|
|
48
|
+
intervalRef.current = null;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Start quality monitoring
|
|
53
|
+
const startMonitoring = async () => {
|
|
54
|
+
try {
|
|
55
|
+
await collectQualityMetrics();
|
|
56
|
+
|
|
57
|
+
// Set up periodic collection every 5 seconds
|
|
58
|
+
intervalRef.current = setInterval(collectQualityMetrics, 5000);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
logger.error("Failed to start quality monitoring", { error });
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const collectQualityMetrics = async () => {
|
|
65
|
+
try {
|
|
66
|
+
const newQuality = await getCallQualityMetrics();
|
|
67
|
+
if (newQuality) {
|
|
68
|
+
setQuality(newQuality);
|
|
69
|
+
setHistory((prev) => {
|
|
70
|
+
const updated = [...prev, newQuality];
|
|
71
|
+
// Keep only last 50 entries (about 4 minutes of history)
|
|
72
|
+
return updated.slice(-50);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
logger.error("Failed to collect quality metrics", { error });
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
startMonitoring();
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
if (intervalRef.current) {
|
|
84
|
+
clearInterval(intervalRef.current);
|
|
85
|
+
intervalRef.current = null;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}, [isConnected, sdk.livekit]);
|
|
89
|
+
|
|
90
|
+
const getCallQualityMetrics = async (): Promise<CallQuality | null> => {
|
|
91
|
+
const room = sdk.livekit?.room;
|
|
92
|
+
if (!room || !room.localParticipant) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Get WebRTC stats
|
|
98
|
+
const stats = await getWebRTCStats(room);
|
|
99
|
+
if (!stats) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Calculate overall quality based on metrics
|
|
104
|
+
const overall = calculateOverallQuality(stats);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
overall,
|
|
108
|
+
metrics: {
|
|
109
|
+
latency: stats.latency,
|
|
110
|
+
packetLoss: stats.packetLoss,
|
|
111
|
+
bandwidth: {
|
|
112
|
+
upload: stats.bandwidth.upload,
|
|
113
|
+
download: stats.bandwidth.download,
|
|
114
|
+
},
|
|
115
|
+
...(stats.resolution ? { resolution: stats.resolution } : {}),
|
|
116
|
+
},
|
|
117
|
+
timestamp: Date.now(),
|
|
118
|
+
};
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logger.error("Error getting quality metrics", { error });
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return { quality, history };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Hook for monitoring call quality with custom intervals
|
|
130
|
+
*/
|
|
131
|
+
export function useCallQualityWithConfig(
|
|
132
|
+
intervalMs = 5000,
|
|
133
|
+
maxHistorySize = 50
|
|
134
|
+
): {
|
|
135
|
+
quality: CallQuality | null;
|
|
136
|
+
history: CallQuality[];
|
|
137
|
+
} {
|
|
138
|
+
const sdk = useSdk();
|
|
139
|
+
const isConnected = useRtcStore((state) => state.connection.connected);
|
|
140
|
+
const [quality, setQuality] = useState<CallQuality | null>(null);
|
|
141
|
+
const [history, setHistory] = useState<CallQuality[]>([]);
|
|
142
|
+
const intervalRef = useRef<number | null>(null);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (!isConnected || !sdk.livekit?.room) {
|
|
146
|
+
setQuality(null);
|
|
147
|
+
if (intervalRef.current) {
|
|
148
|
+
clearInterval(intervalRef.current);
|
|
149
|
+
}
|
|
150
|
+
intervalRef.current = null;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const collectMetrics = async () => {
|
|
155
|
+
const room = sdk.livekit?.room;
|
|
156
|
+
if (!room) return;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const stats = await getWebRTCStats(room);
|
|
160
|
+
if (!stats) return;
|
|
161
|
+
|
|
162
|
+
const newQuality: CallQuality = {
|
|
163
|
+
overall: calculateOverallQuality(stats),
|
|
164
|
+
metrics: {
|
|
165
|
+
latency: stats.latency,
|
|
166
|
+
packetLoss: stats.packetLoss,
|
|
167
|
+
bandwidth: {
|
|
168
|
+
upload: stats.bandwidth.upload,
|
|
169
|
+
download: stats.bandwidth.download,
|
|
170
|
+
},
|
|
171
|
+
...(stats.resolution ? { resolution: stats.resolution } : {}),
|
|
172
|
+
},
|
|
173
|
+
timestamp: Date.now(),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
setQuality(newQuality);
|
|
177
|
+
setHistory((prev) => {
|
|
178
|
+
const updated = [...prev, newQuality];
|
|
179
|
+
return updated.slice(-maxHistorySize);
|
|
180
|
+
});
|
|
181
|
+
} catch (error) {
|
|
182
|
+
logger.error("Failed to collect quality metrics", { error });
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Initial collection
|
|
187
|
+
collectMetrics();
|
|
188
|
+
|
|
189
|
+
// Set up interval
|
|
190
|
+
intervalRef.current = setInterval(collectMetrics, intervalMs);
|
|
191
|
+
|
|
192
|
+
return () => {
|
|
193
|
+
if (intervalRef.current) {
|
|
194
|
+
clearInterval(intervalRef.current);
|
|
195
|
+
intervalRef.current = null;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}, [isConnected, sdk.livekit, intervalMs, maxHistorySize]);
|
|
199
|
+
|
|
200
|
+
return { quality, history };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Hook for getting quality metrics for a specific participant
|
|
205
|
+
*/
|
|
206
|
+
export function useParticipantQuality(
|
|
207
|
+
participantId?: string
|
|
208
|
+
): CallQuality | null {
|
|
209
|
+
const sdk = useSdk();
|
|
210
|
+
const isConnected = useRtcStore((state) => state.connection.connected);
|
|
211
|
+
const [quality, setQuality] = useState<CallQuality | null>(null);
|
|
212
|
+
const intervalRef = useRef<number | null>(null);
|
|
213
|
+
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
if (!isConnected || !sdk.livekit?.room) {
|
|
216
|
+
setQuality(null);
|
|
217
|
+
if (intervalRef.current) {
|
|
218
|
+
clearInterval(intervalRef.current);
|
|
219
|
+
}
|
|
220
|
+
intervalRef.current = null;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const room = sdk.livekit.room;
|
|
225
|
+
const participant = participantId
|
|
226
|
+
? room.remoteParticipants.get(participantId) || room.localParticipant
|
|
227
|
+
: room.localParticipant;
|
|
228
|
+
|
|
229
|
+
if (!participant) {
|
|
230
|
+
setQuality(null);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const collectParticipantMetrics = async () => {
|
|
235
|
+
try {
|
|
236
|
+
const stats = await getParticipantStats(room, participant);
|
|
237
|
+
if (stats) {
|
|
238
|
+
const newQuality: CallQuality = {
|
|
239
|
+
overall: calculateOverallQuality(stats),
|
|
240
|
+
metrics: {
|
|
241
|
+
latency: stats.latency,
|
|
242
|
+
packetLoss: stats.packetLoss,
|
|
243
|
+
bandwidth: {
|
|
244
|
+
upload: stats.bandwidth.upload,
|
|
245
|
+
download: stats.bandwidth.download,
|
|
246
|
+
},
|
|
247
|
+
...(stats.resolution ? { resolution: stats.resolution } : {}),
|
|
248
|
+
},
|
|
249
|
+
timestamp: Date.now(),
|
|
250
|
+
};
|
|
251
|
+
setQuality(newQuality);
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
logger.error("Failed to collect participant quality metrics", {
|
|
255
|
+
error,
|
|
256
|
+
participantId,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Initial collection
|
|
262
|
+
collectParticipantMetrics();
|
|
263
|
+
|
|
264
|
+
// Set up interval
|
|
265
|
+
intervalRef.current = setInterval(collectParticipantMetrics, 5000);
|
|
266
|
+
|
|
267
|
+
return () => {
|
|
268
|
+
if (intervalRef.current) {
|
|
269
|
+
clearInterval(intervalRef.current);
|
|
270
|
+
intervalRef.current = null;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}, [isConnected, sdk.livekit, participantId]);
|
|
274
|
+
|
|
275
|
+
return quality;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Helper functions
|
|
279
|
+
interface WebRTCStats {
|
|
280
|
+
latency: number;
|
|
281
|
+
packetLoss: number;
|
|
282
|
+
bandwidth: {
|
|
283
|
+
upload: number;
|
|
284
|
+
download: number;
|
|
285
|
+
};
|
|
286
|
+
resolution?: {
|
|
287
|
+
width: number;
|
|
288
|
+
height: number;
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function getWebRTCStats(room: any): Promise<WebRTCStats | null> {
|
|
293
|
+
try {
|
|
294
|
+
// Get the underlying WebRTC peer connection
|
|
295
|
+
const pc = room.engine?.publisher?.pc || room.engine?.subscriber?.pc;
|
|
296
|
+
if (!pc) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const stats = await pc.getStats();
|
|
301
|
+
const statsArray = Array.from(stats.values());
|
|
302
|
+
|
|
303
|
+
// Extract relevant metrics
|
|
304
|
+
let latency = 0;
|
|
305
|
+
let packetLoss = 0;
|
|
306
|
+
let uploadBandwidth = 0;
|
|
307
|
+
let downloadBandwidth = 0;
|
|
308
|
+
let resolution: { width: number; height: number } | undefined;
|
|
309
|
+
|
|
310
|
+
for (const stat of statsArray) {
|
|
311
|
+
const s = stat as any;
|
|
312
|
+
// RTT (Round Trip Time) for latency
|
|
313
|
+
if (s.type === "candidate-pair" && s.state === "succeeded") {
|
|
314
|
+
latency = s.currentRoundTripTime ? s.currentRoundTripTime * 1000 : 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Packet loss from outbound RTP
|
|
318
|
+
if (s.type === "outbound-rtp" && s.mediaType === "audio") {
|
|
319
|
+
if (s.packetsLost && s.packetsSent) {
|
|
320
|
+
packetLoss = (s.packetsLost / s.packetsSent) * 100;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Upload bandwidth (estimate from bytes sent)
|
|
324
|
+
if (s.bytesSent && s.timestamp) {
|
|
325
|
+
uploadBandwidth = (s.bytesSent * 8) / 1000; // Convert to kbps
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Download bandwidth from inbound RTP
|
|
330
|
+
if (s.type === "inbound-rtp" && s.mediaType === "audio") {
|
|
331
|
+
if (s.bytesReceived && s.timestamp) {
|
|
332
|
+
downloadBandwidth = (s.bytesReceived * 8) / 1000; // Convert to kbps
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Video resolution from outbound video
|
|
337
|
+
if (s.type === "outbound-rtp" && s.mediaType === "video") {
|
|
338
|
+
if (s.frameWidth && s.frameHeight) {
|
|
339
|
+
resolution = {
|
|
340
|
+
width: s.frameWidth,
|
|
341
|
+
height: s.frameHeight,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const result: WebRTCStats = {
|
|
348
|
+
latency: Math.round(latency),
|
|
349
|
+
packetLoss: Math.round(packetLoss * 100) / 100, // Round to 2 decimal places
|
|
350
|
+
bandwidth: {
|
|
351
|
+
upload: Math.round(uploadBandwidth),
|
|
352
|
+
download: Math.round(downloadBandwidth),
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
if (resolution) {
|
|
357
|
+
result.resolution = resolution;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return result;
|
|
361
|
+
} catch (error) {
|
|
362
|
+
logger.error("Error getting WebRTC stats", { error });
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function getParticipantStats(
|
|
368
|
+
room: any,
|
|
369
|
+
participant: any
|
|
370
|
+
): Promise<WebRTCStats | null> {
|
|
371
|
+
// For now, return the same stats as the room
|
|
372
|
+
// In a more sophisticated implementation, this could get participant-specific stats
|
|
373
|
+
return getWebRTCStats(room);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function calculateOverallQuality(stats: WebRTCStats): CallQuality["overall"] {
|
|
377
|
+
let score = 100;
|
|
378
|
+
|
|
379
|
+
// Deduct points for high latency
|
|
380
|
+
if (stats.latency > 300) {
|
|
381
|
+
score -= 40; // Very high latency
|
|
382
|
+
} else if (stats.latency > 150) {
|
|
383
|
+
score -= 20; // High latency
|
|
384
|
+
} else if (stats.latency > 100) {
|
|
385
|
+
score -= 10; // Moderate latency
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Deduct points for packet loss
|
|
389
|
+
if (stats.packetLoss > 5) {
|
|
390
|
+
score -= 30; // High packet loss
|
|
391
|
+
} else if (stats.packetLoss > 2) {
|
|
392
|
+
score -= 15; // Moderate packet loss
|
|
393
|
+
} else if (stats.packetLoss > 1) {
|
|
394
|
+
score -= 5; // Low packet loss
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Deduct points for low bandwidth
|
|
398
|
+
const totalBandwidth = stats.bandwidth.upload + stats.bandwidth.download;
|
|
399
|
+
if (totalBandwidth < 50) {
|
|
400
|
+
score -= 25; // Very low bandwidth
|
|
401
|
+
} else if (totalBandwidth < 100) {
|
|
402
|
+
score -= 10; // Low bandwidth
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Determine overall quality
|
|
406
|
+
if (score >= 80) {
|
|
407
|
+
return "excellent";
|
|
408
|
+
}
|
|
409
|
+
if (score >= 60) {
|
|
410
|
+
return "good";
|
|
411
|
+
}
|
|
412
|
+
if (score >= 30) {
|
|
413
|
+
return "poor";
|
|
414
|
+
}
|
|
415
|
+
return "failed";
|
|
416
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useRtcStore } from "../state/store";
|
|
2
|
+
import type { IncomingCallInfo, SessionStatus } from "../state/types";
|
|
3
|
+
|
|
4
|
+
export interface CallState {
|
|
5
|
+
id: string | undefined;
|
|
6
|
+
status: SessionStatus;
|
|
7
|
+
mode: "AUDIO" | "VIDEO" | undefined;
|
|
8
|
+
roomName: string | undefined;
|
|
9
|
+
incomingCall: IncomingCallInfo | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useCallState(): CallState {
|
|
13
|
+
const session = useRtcStore((state) => state.session);
|
|
14
|
+
const incomingCall = useRtcStore((state) => state.incomingCall);
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
id: session.id,
|
|
18
|
+
status: session.status,
|
|
19
|
+
mode: session.mode,
|
|
20
|
+
roomName: session.livekitInfo?.roomName,
|
|
21
|
+
incomingCall,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useRtcStore } from "../state/store";
|
|
2
|
+
|
|
3
|
+
export interface ConnectionState {
|
|
4
|
+
connected: boolean;
|
|
5
|
+
reconnecting: boolean;
|
|
6
|
+
quality?: "excellent" | "good" | "poor" | "lost";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function useConnection(): ConnectionState {
|
|
10
|
+
return useRtcStore((state) => state.connection);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useIsConnected(): boolean {
|
|
14
|
+
return useRtcStore((state) => state.connection.connected);
|
|
15
|
+
}
|