@aether-stack-dev/client-sdk 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -220
  3. package/dist/AStackCSRClient.d.ts +26 -9
  4. package/dist/AnalyticsCollector.d.ts +1 -2
  5. package/dist/BillingMonitor.d.ts +5 -4
  6. package/dist/ConnectionStateManager.d.ts +4 -3
  7. package/dist/{WebRTCManager.d.ts → MediaManager.d.ts} +1 -6
  8. package/dist/PerformanceMonitor.d.ts +2 -3
  9. package/dist/SecurityLogger.d.ts +1 -5
  10. package/dist/SupabaseSignalingClient.d.ts +0 -1
  11. package/dist/UsageTracker.d.ts +0 -1
  12. package/dist/__tests__/setup.d.ts +12 -2
  13. package/dist/audio/AudioPlayer.d.ts +1 -1
  14. package/dist/audio/index.d.ts +0 -1
  15. package/dist/avatar/TalkingHeadAvatar.d.ts +0 -1
  16. package/dist/avatar/VRMAvatar.d.ts +0 -1
  17. package/dist/avatar/constants.d.ts +0 -1
  18. package/dist/avatar/index.d.ts +0 -1
  19. package/dist/core.d.ts +2 -7
  20. package/dist/index.d.ts +3 -15
  21. package/dist/index.esm.js +283 -1868
  22. package/dist/index.js +283 -1871
  23. package/dist/react/index.d.ts +0 -3
  24. package/dist/react/useAStackCSR.d.ts +0 -1
  25. package/dist/react.esm.js +263 -2088
  26. package/dist/react.js +261 -2087
  27. package/dist/types.d.ts +20 -19
  28. package/package.json +4 -4
  29. package/dist/AStackCSRClient.d.ts.map +0 -1
  30. package/dist/AStackClient.d.ts +0 -90
  31. package/dist/AStackClient.d.ts.map +0 -1
  32. package/dist/AStackEventSetup.d.ts +0 -35
  33. package/dist/AStackEventSetup.d.ts.map +0 -1
  34. package/dist/AStackMediaController.d.ts +0 -34
  35. package/dist/AStackMediaController.d.ts.map +0 -1
  36. package/dist/AnalyticsCollector.d.ts.map +0 -1
  37. package/dist/BillingMonitor.d.ts.map +0 -1
  38. package/dist/ConnectionStateManager.d.ts.map +0 -1
  39. package/dist/PerformanceMonitor.d.ts.map +0 -1
  40. package/dist/SecurityLogger.d.ts.map +0 -1
  41. package/dist/SessionManager.d.ts.map +0 -1
  42. package/dist/SupabaseSignalingClient.d.ts.map +0 -1
  43. package/dist/UsageTracker.d.ts.map +0 -1
  44. package/dist/WebRTCManager.d.ts.map +0 -1
  45. package/dist/__tests__/setup.d.ts.map +0 -1
  46. package/dist/audio/AudioPlayer.d.ts.map +0 -1
  47. package/dist/audio/index.d.ts.map +0 -1
  48. package/dist/avatar/TalkingHeadAvatar.d.ts.map +0 -1
  49. package/dist/avatar/VRMAvatar.d.ts.map +0 -1
  50. package/dist/avatar/constants.d.ts.map +0 -1
  51. package/dist/avatar/index.d.ts.map +0 -1
  52. package/dist/core.d.ts.map +0 -1
  53. package/dist/index.d.ts.map +0 -1
  54. package/dist/index.esm.js.map +0 -1
  55. package/dist/index.js.map +0 -1
  56. package/dist/react/index.d.ts.map +0 -1
  57. package/dist/react/useAStack.d.ts +0 -34
  58. package/dist/react/useAStack.d.ts.map +0 -1
  59. package/dist/react/useAStackCSR.d.ts.map +0 -1
  60. package/dist/react.esm.js.map +0 -1
  61. package/dist/react.js.map +0 -1
  62. package/dist/types.d.ts.map +0 -1
package/dist/react.js CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  var react = require('react');
4
4
  var eventemitter3 = require('eventemitter3');
5
- var supabaseJs = require('@supabase/supabase-js');
6
5
  var jsxRuntime = require('react/jsx-runtime');
7
6
  var THREE = require('three');
8
7
  var GLTFLoader_js = require('three/examples/jsm/loaders/GLTFLoader.js');
@@ -27,2031 +26,6 @@ function _interopNamespaceDefault(e) {
27
26
 
28
27
  var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
29
28
 
30
- class WebRTCManager extends eventemitter3.EventEmitter {
31
- constructor(config) {
32
- super();
33
- this.localStream = null;
34
- this.config = config;
35
- }
36
- async initialize() {
37
- this.emit('initialized');
38
- }
39
- async getStats() {
40
- return null;
41
- }
42
- async getUserMedia() {
43
- const constraints = {
44
- audio: this.config.enableAudio !== false ? {
45
- echoCancellation: this.config.audio?.echoCancellation ?? true,
46
- noiseSuppression: this.config.audio?.noiseSuppression ?? true,
47
- autoGainControl: this.config.audio?.autoGainControl ?? true
48
- } : false
49
- };
50
- return navigator.mediaDevices.getUserMedia(constraints);
51
- }
52
- async addLocalStream(stream) {
53
- this.localStream = stream;
54
- this.emit('localStreamAdded', stream);
55
- }
56
- removeLocalStream(streamId) {
57
- if (this.localStream) {
58
- this.localStream.getTracks().forEach(track => {
59
- if (track.id === streamId)
60
- track.stop();
61
- });
62
- }
63
- }
64
- getLocalStreams() {
65
- if (!this.localStream)
66
- return [];
67
- return this.localStream.getTracks().map(track => ({
68
- id: track.id,
69
- kind: track.kind,
70
- active: track.enabled,
71
- track
72
- }));
73
- }
74
- getRemoteStreams() {
75
- return [];
76
- }
77
- async getMediaDevices() {
78
- return navigator.mediaDevices.enumerateDevices();
79
- }
80
- async switchAudioInput(deviceId) {
81
- if (this.localStream) {
82
- this.localStream.getAudioTracks().forEach(track => track.stop());
83
- }
84
- const stream = await navigator.mediaDevices.getUserMedia({
85
- audio: { deviceId: { exact: deviceId } }
86
- });
87
- this.localStream = stream;
88
- this.emit('localStreamAdded', stream);
89
- }
90
- muteAudio() {
91
- if (this.localStream) {
92
- this.localStream.getAudioTracks().forEach(track => { track.enabled = false; });
93
- }
94
- }
95
- unmuteAudio() {
96
- if (this.localStream) {
97
- this.localStream.getAudioTracks().forEach(track => { track.enabled = true; });
98
- }
99
- }
100
- muteVideo() {
101
- if (this.localStream) {
102
- this.localStream.getVideoTracks().forEach(track => { track.enabled = false; });
103
- }
104
- }
105
- unmuteVideo() {
106
- if (this.localStream) {
107
- this.localStream.getVideoTracks().forEach(track => { track.enabled = true; });
108
- }
109
- }
110
- async createOffer() {
111
- console.warn('WebRTC is deprecated. Use WebSocket-based AStackCSRClient instead.');
112
- return { type: 'offer', sdp: '' };
113
- }
114
- async createAnswer(remoteSdp) {
115
- console.warn('WebRTC is deprecated. Use WebSocket-based AStackCSRClient instead.');
116
- return { type: 'answer', sdp: '' };
117
- }
118
- async handleAnswer(answer) {
119
- console.warn('WebRTC is deprecated. Use WebSocket-based AStackCSRClient instead.');
120
- }
121
- async addIceCandidate(candidate) {
122
- console.warn('WebRTC is deprecated. Use WebSocket-based AStackCSRClient instead.');
123
- }
124
- async close() {
125
- if (this.localStream) {
126
- this.localStream.getTracks().forEach(track => track.stop());
127
- this.localStream = null;
128
- }
129
- this.emit('closed');
130
- }
131
- }
132
-
133
- class AStackError extends Error {
134
- constructor(message, code) {
135
- super(message);
136
- this.name = 'AStackError';
137
- this.code = code;
138
- }
139
- }
140
- const ErrorCodes = {
141
- INVALID_API_KEY: 'INVALID_API_KEY',
142
- INVALID_TOKEN: 'INVALID_TOKEN',
143
- AUTHENTICATION_FAILED: 'AUTHENTICATION_FAILED',
144
- SESSION_EXPIRED: 'SESSION_EXPIRED',
145
- SESSION_CREATION_FAILED: 'SESSION_CREATION_FAILED',
146
- CONNECTION_LOST: 'CONNECTION_LOST',
147
- SIGNALING_ERROR: 'SIGNALING_ERROR',
148
- NETWORK_ERROR: 'NETWORK_ERROR',
149
- MEDIA_ACCESS_DENIED: 'MEDIA_ACCESS_DENIED',
150
- AUDIO_ERROR: 'AUDIO_ERROR',
151
- RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
152
- INSUFFICIENT_CREDITS: 'INSUFFICIENT_CREDITS',
153
- BILLING_ERROR: 'BILLING_ERROR'
154
- };
155
-
156
- class SupabaseSignalingClient extends eventemitter3.EventEmitter {
157
- constructor(config) {
158
- super();
159
- this.channel = null;
160
- this.sessionId = null;
161
- this.channelName = null;
162
- this.wsToken = null;
163
- this.channelId = null;
164
- this.connected = false;
165
- this.reconnectAttempts = 0;
166
- this.heartbeatInterval = null;
167
- this.config = config;
168
- this.maxReconnectAttempts = config.maxRetries || 3;
169
- this.reconnectDelay = config.reconnectDelay || 1000;
170
- this.supabase = supabaseJs.createClient(config.supabaseUrl || process.env.NEXT_PUBLIC_SUPABASE_URL || '', config.supabaseAnonKey || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '', {
171
- realtime: {
172
- params: {
173
- eventsPerSecond: 10
174
- }
175
- }
176
- });
177
- }
178
- async connect(sessionId, channelName, wsToken) {
179
- this.sessionId = sessionId;
180
- this.channelName = channelName;
181
- this.wsToken = wsToken;
182
- return new Promise(async (resolve, reject) => {
183
- try {
184
- const { data: channelData, error } = await this.supabase
185
- .from('signaling_channels')
186
- .select('id')
187
- .eq('channel_name', channelName)
188
- .eq('ws_token', wsToken)
189
- .single();
190
- if (error || !channelData) {
191
- throw new AStackError('Invalid channel credentials', ErrorCodes.SIGNALING_ERROR);
192
- }
193
- this.channelId = channelData.id;
194
- this.channel = this.supabase.channel(channelName);
195
- this.channel
196
- .on('postgres_changes', {
197
- event: 'INSERT',
198
- schema: 'public',
199
- table: 'signaling_messages',
200
- filter: `channel_id=eq.${this.channelId}`
201
- }, (payload) => {
202
- this.handleSignalingMessage(payload.new);
203
- })
204
- .on('presence', { event: 'sync' }, () => {
205
- const state = this.channel?.presenceState();
206
- this.handlePresenceSync(state);
207
- })
208
- .on('presence', { event: 'join' }, ({ key, newPresences }) => {
209
- this.emit('userJoined', { key, presences: newPresences });
210
- })
211
- .on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
212
- this.emit('userLeft', { key, presences: leftPresences });
213
- })
214
- .subscribe(async (status) => {
215
- if (status === 'SUBSCRIBED') {
216
- this.connected = true;
217
- this.reconnectAttempts = 0;
218
- await this.updateChannelStatus(true);
219
- await this.channel.track({
220
- user_type: 'client',
221
- user_id: sessionId,
222
- status: 'online',
223
- last_seen: new Date().toISOString(),
224
- metadata: {
225
- userAgent: navigator.userAgent,
226
- capabilities: {
227
- audio: this.config.enableAudio !== false,
228
- video: this.config.enableVideo === true,
229
- text: this.config.enableText !== false
230
- }
231
- }
232
- });
233
- this.startHeartbeat();
234
- this.emit('sessionJoined', { sessionId, channelName });
235
- resolve();
236
- }
237
- else if (status === 'CHANNEL_ERROR') {
238
- reject(new AStackError('Failed to subscribe to signaling channel', ErrorCodes.SIGNALING_ERROR));
239
- }
240
- });
241
- }
242
- catch (error) {
243
- reject(new AStackError(`Failed to connect to signaling: ${error}`, ErrorCodes.SIGNALING_ERROR));
244
- }
245
- });
246
- }
247
- async handleSignalingMessage(message) {
248
- if (message.sender_type === 'client') {
249
- return;
250
- }
251
- switch (message.message_type) {
252
- case 'audio':
253
- this.emit('audioResponse', message.payload);
254
- break;
255
- case 'text':
256
- this.emit('textResponse', message.payload);
257
- break;
258
- case 'control':
259
- this.emit('controlMessage', message.payload);
260
- break;
261
- case 'ready':
262
- this.emit('workerReady', message.payload);
263
- break;
264
- case 'error':
265
- this.emit('error', new AStackError(message.payload.message || 'Worker error', ErrorCodes.SIGNALING_ERROR));
266
- break;
267
- }
268
- }
269
- handlePresenceSync(state) {
270
- if (!state)
271
- return;
272
- const presences = Object.values(state);
273
- const workerPresent = presences.some((presence) => presence.user_type === 'worker' && presence.status === 'online');
274
- if (workerPresent) {
275
- this.emit('workerConnected');
276
- }
277
- else {
278
- this.emit('workerDisconnected');
279
- }
280
- }
281
- async updateChannelStatus(connected) {
282
- if (!this.channelId)
283
- return;
284
- try {
285
- await this.supabase
286
- .from('signaling_channels')
287
- .update({ client_connected: connected })
288
- .eq('id', this.channelId);
289
- }
290
- catch (error) { }
291
- }
292
- async sendText(text) {
293
- await this.sendSignalingMessage('text', { text });
294
- }
295
- async sendAudio(audioData, metadata) {
296
- await this.sendSignalingMessage('audio', { audio: audioData, ...metadata });
297
- }
298
- async sendControl(action, data) {
299
- await this.sendSignalingMessage('control', { action, ...data });
300
- }
301
- async sendSignalingMessage(messageType, payload) {
302
- if (!this.channelId || !this.connected) {
303
- throw new AStackError('Not connected to signaling channel', ErrorCodes.SIGNALING_ERROR);
304
- }
305
- try {
306
- const { error } = await this.supabase
307
- .from('signaling_messages')
308
- .insert({
309
- channel_id: this.channelId,
310
- sender_type: 'client',
311
- message_type: messageType,
312
- payload
313
- });
314
- if (error) {
315
- throw new AStackError(`Failed to send ${messageType}: ${error.message}`, ErrorCodes.SIGNALING_ERROR);
316
- }
317
- }
318
- catch (error) {
319
- throw error;
320
- }
321
- }
322
- sendTextMessage(text) {
323
- this.sendText(text).catch(() => { });
324
- }
325
- sendAudioData(audioBlob) {
326
- const reader = new FileReader();
327
- reader.onload = () => {
328
- const base64 = reader.result.split(',')[1];
329
- this.sendAudio(base64).catch(() => { });
330
- };
331
- reader.readAsDataURL(audioBlob);
332
- }
333
- startHeartbeat() {
334
- if (this.heartbeatInterval) {
335
- clearInterval(this.heartbeatInterval);
336
- }
337
- const interval = this.config.heartbeatInterval || 30000;
338
- this.heartbeatInterval = setInterval(async () => {
339
- if (this.channel && this.connected) {
340
- try {
341
- await this.channel.track({
342
- user_type: 'client',
343
- user_id: this.sessionId,
344
- status: 'online',
345
- last_seen: new Date().toISOString()
346
- });
347
- }
348
- catch (error) { }
349
- }
350
- }, interval);
351
- }
352
- stopHeartbeat() {
353
- if (this.heartbeatInterval) {
354
- clearInterval(this.heartbeatInterval);
355
- this.heartbeatInterval = null;
356
- }
357
- }
358
- async attemptReconnect() {
359
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
360
- this.emit('error', new AStackError('Maximum reconnection attempts reached', ErrorCodes.CONNECTION_LOST));
361
- return;
362
- }
363
- this.reconnectAttempts++;
364
- const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
365
- this.emit('reconnecting', this.reconnectAttempts);
366
- setTimeout(async () => {
367
- if (!this.connected && this.sessionId && this.channelName && this.wsToken) {
368
- try {
369
- await this.connect(this.sessionId, this.channelName, this.wsToken);
370
- this.emit('reconnected');
371
- }
372
- catch (error) {
373
- this.attemptReconnect();
374
- }
375
- }
376
- }, delay);
377
- }
378
- async leaveSession() {
379
- if (this.channel) {
380
- try {
381
- await this.channel.untrack();
382
- await this.updateChannelStatus(false);
383
- }
384
- catch (error) { }
385
- }
386
- }
387
- isConnected() {
388
- return this.connected;
389
- }
390
- getSessionId() {
391
- return this.sessionId;
392
- }
393
- async disconnect() {
394
- this.connected = false;
395
- this.stopHeartbeat();
396
- if (this.channel) {
397
- await this.leaveSession();
398
- await this.channel.unsubscribe();
399
- this.channel = null;
400
- }
401
- this.removeAllListeners();
402
- }
403
- }
404
-
405
- class SessionManager {
406
- constructor(apiEndpoint, sessionExpirationWarning = 1, onSessionExpiring) {
407
- this.sessionCredentials = null;
408
- this.workerInfo = null;
409
- this.expirationTimer = null;
410
- this.apiEndpoint = apiEndpoint;
411
- this.sessionExpirationWarning = sessionExpirationWarning;
412
- this.onSessionExpiring = onSessionExpiring;
413
- }
414
- async createSession(request) {
415
- try {
416
- const response = await fetch(`${this.apiEndpoint}/functions/v1/session`, {
417
- method: 'POST',
418
- headers: {
419
- 'Content-Type': 'application/json',
420
- 'Authorization': `Bearer ${request.apiKey}`
421
- },
422
- body: JSON.stringify({
423
- organizationId: request.organizationId,
424
- endUserId: request.endUserId,
425
- metadata: request.metadata,
426
- workerPreferences: request.workerPreferences
427
- })
428
- });
429
- if (!response.ok) {
430
- const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
431
- if (response.status === 401) {
432
- throw new AStackError('Invalid API key', ErrorCodes.INVALID_API_KEY);
433
- }
434
- else if (response.status === 429) {
435
- throw new AStackError('Rate limit exceeded', ErrorCodes.RATE_LIMIT_EXCEEDED);
436
- }
437
- else if (response.status === 402) {
438
- throw new AStackError('Insufficient credits', ErrorCodes.INSUFFICIENT_CREDITS);
439
- }
440
- throw new AStackError(errorData.error || 'Session creation failed', ErrorCodes.SESSION_CREATION_FAILED);
441
- }
442
- const data = await response.json();
443
- const expiresAt = new Date(Date.now() + (data.expiresIn * 1000));
444
- this.sessionCredentials = {
445
- sessionId: data.sessionId,
446
- wsToken: data.wsToken,
447
- channelName: data.channelName,
448
- workerUrl: data.workerUrl,
449
- expiresAt
450
- };
451
- if (data.workerInfo) {
452
- this.workerInfo = data.workerInfo;
453
- }
454
- this.scheduleExpirationWarning();
455
- return this.sessionCredentials;
456
- }
457
- catch (error) {
458
- if (error instanceof AStackError) {
459
- throw error;
460
- }
461
- throw new AStackError(`Failed to create session: ${error instanceof Error ? error.message : 'Unknown error'}`, ErrorCodes.SESSION_CREATION_FAILED);
462
- }
463
- }
464
- async renewSession(apiKey) {
465
- if (!this.sessionCredentials) {
466
- throw new AStackError('No active session to renew', ErrorCodes.SESSION_EXPIRED);
467
- }
468
- const request = {
469
- apiKey,
470
- organizationId: this.sessionCredentials.sessionId.split('-')[0],
471
- endUserId: undefined,
472
- metadata: { previousSessionId: this.sessionCredentials.sessionId }
473
- };
474
- return this.createSession(request);
475
- }
476
- scheduleExpirationWarning() {
477
- if (this.expirationTimer) {
478
- clearTimeout(this.expirationTimer);
479
- }
480
- if (!this.sessionCredentials || !this.onSessionExpiring) {
481
- return;
482
- }
483
- const now = Date.now();
484
- const expirationTime = this.sessionCredentials.expiresAt.getTime();
485
- const warningTime = expirationTime - (this.sessionExpirationWarning * 60 * 1000);
486
- if (warningTime > now) {
487
- this.expirationTimer = setTimeout(() => {
488
- if (this.onSessionExpiring && this.sessionCredentials) {
489
- const minutesRemaining = Math.floor((this.sessionCredentials.expiresAt.getTime() - Date.now()) / 60000);
490
- this.onSessionExpiring(minutesRemaining);
491
- }
492
- }, warningTime - now);
493
- }
494
- }
495
- getCredentials() {
496
- return this.sessionCredentials;
497
- }
498
- isSessionValid() {
499
- if (!this.sessionCredentials) {
500
- return false;
501
- }
502
- return this.sessionCredentials.expiresAt.getTime() > Date.now();
503
- }
504
- getTimeUntilExpiration() {
505
- if (!this.sessionCredentials) {
506
- return 0;
507
- }
508
- const remaining = this.sessionCredentials.expiresAt.getTime() - Date.now();
509
- return Math.max(0, remaining);
510
- }
511
- clearSession() {
512
- this.sessionCredentials = null;
513
- this.workerInfo = null;
514
- if (this.expirationTimer) {
515
- clearTimeout(this.expirationTimer);
516
- this.expirationTimer = null;
517
- }
518
- }
519
- getWorkerInfo() {
520
- return this.workerInfo;
521
- }
522
- destroy() {
523
- this.clearSession();
524
- }
525
- }
526
-
527
- class BillingMonitor {
528
- constructor(apiEndpoint, apiKey) {
529
- this.apiEndpoint = apiEndpoint;
530
- this.apiKey = apiKey;
531
- this.updateCallbacks = [];
532
- this.alertCallbacks = [];
533
- this.warningThresholds = {
534
- creditBalance: 10, // Warn when credits drop below 10
535
- usagePercentage: 80 // Warn when usage reaches 80% of rate limit
536
- };
537
- }
538
- async fetchBillingInfo() {
539
- try {
540
- const response = await fetch(`${this.apiEndpoint}/billing/info`, {
541
- headers: {
542
- 'Authorization': `Bearer ${this.apiKey}`,
543
- 'Content-Type': 'application/json'
544
- }
545
- });
546
- if (!response.ok) {
547
- throw new Error(`Failed to fetch billing info: ${response.statusText}`);
548
- }
549
- const info = await response.json();
550
- this.updateBillingInfo(info);
551
- return info;
552
- }
553
- catch (error) {
554
- throw error;
555
- }
556
- }
557
- updateBillingInfo(info) {
558
- const previousInfo = this.billingInfo;
559
- this.billingInfo = info;
560
- this.updateCallbacks.forEach(callback => callback(info));
561
- this.checkForAlerts(info, previousInfo);
562
- }
563
- checkForAlerts(current, previous) {
564
- if (current.creditBalance < this.warningThresholds.creditBalance) {
565
- this.triggerAlert({
566
- type: 'low_balance',
567
- details: {
568
- currentBalance: current.creditBalance,
569
- threshold: this.warningThresholds.creditBalance
570
- }
571
- });
572
- }
573
- const usagePercentage = (current.currentUsage / current.rateLimit) * 100;
574
- if (usagePercentage >= this.warningThresholds.usagePercentage) {
575
- this.triggerAlert({
576
- type: 'limit_exceeded',
577
- details: {
578
- currentUsage: current.currentUsage,
579
- rateLimit: current.rateLimit,
580
- percentage: usagePercentage
581
- }
582
- });
583
- }
584
- if (previous && previous.creditBalance - current.creditBalance > 100) {
585
- this.triggerAlert({
586
- type: 'low_balance',
587
- details: {
588
- previousBalance: previous.creditBalance,
589
- currentBalance: current.creditBalance,
590
- spent: previous.creditBalance - current.creditBalance
591
- }
592
- });
593
- }
594
- }
595
- triggerAlert(alert) {
596
- this.alertCallbacks.forEach(callback => callback(alert));
597
- }
598
- startMonitoring(intervalMs = 60000) {
599
- this.stopMonitoring();
600
- this.fetchBillingInfo().catch(() => { });
601
- this.checkInterval = setInterval(() => {
602
- this.fetchBillingInfo().catch(() => { });
603
- }, intervalMs);
604
- }
605
- stopMonitoring() {
606
- if (this.checkInterval) {
607
- clearInterval(this.checkInterval);
608
- this.checkInterval = undefined;
609
- }
610
- }
611
- onUpdate(callback) {
612
- this.updateCallbacks.push(callback);
613
- }
614
- onAlert(callback) {
615
- this.alertCallbacks.push(callback);
616
- }
617
- removeUpdateCallback(callback) {
618
- this.updateCallbacks = this.updateCallbacks.filter(cb => cb !== callback);
619
- }
620
- removeAlertCallback(callback) {
621
- this.alertCallbacks = this.alertCallbacks.filter(cb => cb !== callback);
622
- }
623
- getCurrentBillingInfo() {
624
- return this.billingInfo;
625
- }
626
- setWarningThresholds(thresholds) {
627
- this.warningThresholds = { ...this.warningThresholds, ...thresholds };
628
- }
629
- async checkRateLimit() {
630
- const info = await this.fetchBillingInfo();
631
- return info.currentUsage < info.rateLimit;
632
- }
633
- destroy() {
634
- this.stopMonitoring();
635
- this.updateCallbacks = [];
636
- this.alertCallbacks = [];
637
- }
638
- }
639
-
640
- class SecurityLogger {
641
- constructor(config) {
642
- this.eventQueue = [];
643
- this.config = {
644
- enableLocalLogging: true,
645
- batchSize: 10,
646
- flushInterval: 5000,
647
- sessionId: '',
648
- organizationId: '',
649
- ...config
650
- };
651
- if (this.config.flushInterval > 0) {
652
- this.startAutoFlush();
653
- }
654
- }
655
- setSessionId(sessionId) {
656
- this.config.sessionId = sessionId;
657
- }
658
- logEvent(eventType, severity, details) {
659
- const event = {
660
- eventType,
661
- severity,
662
- details: {
663
- ...details,
664
- sessionId: this.config.sessionId,
665
- organizationId: this.config.organizationId,
666
- userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
667
- timestamp: new Date().toISOString()
668
- },
669
- timestamp: new Date()
670
- };
671
- if (this.config.enableLocalLogging) {
672
- this.logToConsole(event);
673
- }
674
- this.eventQueue.push(event);
675
- if (this.eventQueue.length >= this.config.batchSize) {
676
- this.flush().catch(() => { });
677
- }
678
- }
679
- logAuthFailure(reason, details) {
680
- this.logEvent('auth_failure', 'warning', {
681
- reason,
682
- ...details
683
- });
684
- }
685
- logRateLimit(limit, current, endpoint) {
686
- this.logEvent('rate_limit', 'warning', {
687
- limit,
688
- current,
689
- endpoint,
690
- exceeded: current >= limit
691
- });
692
- }
693
- logInvalidRequest(reason, request) {
694
- this.logEvent('invalid_request', 'warning', {
695
- reason,
696
- request: this.sanitizeRequest(request)
697
- });
698
- }
699
- logSessionHijackAttempt(details) {
700
- this.logEvent('session_hijack', 'critical', details);
701
- }
702
- sanitizeRequest(request) {
703
- if (!request)
704
- return undefined;
705
- const sanitized = { ...request };
706
- const sensitiveFields = ['password', 'token', 'apiKey', 'secret', 'credential'];
707
- for (const field of sensitiveFields) {
708
- if (sanitized[field]) {
709
- sanitized[field] = '[REDACTED]';
710
- }
711
- }
712
- return sanitized;
713
- }
714
- logToConsole(event) {
715
- }
716
- getConsoleLogLevel(severity) {
717
- switch (severity) {
718
- case 'info':
719
- return 'log';
720
- case 'warning':
721
- return 'warn';
722
- case 'error':
723
- case 'critical':
724
- return 'error';
725
- default:
726
- return 'log';
727
- }
728
- }
729
- async flush() {
730
- if (this.eventQueue.length === 0)
731
- return;
732
- const events = [...this.eventQueue];
733
- this.eventQueue = [];
734
- try {
735
- const response = await fetch(`${this.config.apiEndpoint}/security/events`, {
736
- method: 'POST',
737
- headers: {
738
- 'Authorization': `Bearer ${this.config.apiKey}`,
739
- 'Content-Type': 'application/json'
740
- },
741
- body: JSON.stringify({ events })
742
- });
743
- if (!response.ok) {
744
- this.eventQueue.unshift(...events);
745
- }
746
- }
747
- catch (error) {
748
- this.eventQueue.unshift(...events);
749
- }
750
- }
751
- startAutoFlush() {
752
- this.flushInterval = setInterval(() => {
753
- this.flush().catch(() => { });
754
- }, this.config.flushInterval);
755
- }
756
- stopAutoFlush() {
757
- if (this.flushInterval) {
758
- clearInterval(this.flushInterval);
759
- this.flushInterval = undefined;
760
- }
761
- }
762
- async destroy() {
763
- this.stopAutoFlush();
764
- await this.flush();
765
- }
766
- }
767
-
768
- class ConnectionStateManager extends eventemitter3.EventEmitter {
769
- constructor() {
770
- super();
771
- this.stateHistory = [];
772
- this.maxHistorySize = 100;
773
- this.state = {
774
- signaling: 'disconnected',
775
- websocket: 'new',
776
- overall: 'disconnected',
777
- lastActivity: new Date(),
778
- reconnectAttempts: 0,
779
- errors: []
780
- };
781
- this.quality = {
782
- qualityScore: 1.0
783
- };
784
- }
785
- updateSignalingState(state) {
786
- const previousState = this.state.signaling;
787
- this.state.signaling = state;
788
- this.state.lastActivity = new Date();
789
- if (state === 'connected') {
790
- this.state.reconnectAttempts = 0;
791
- }
792
- else if (state === 'reconnecting') {
793
- this.state.reconnectAttempts++;
794
- }
795
- this.updateOverallState();
796
- this.addToHistory();
797
- if (previousState !== state) {
798
- this.emit('signalingStateChange', state, previousState);
799
- }
800
- }
801
- updateWebSocketState(state) {
802
- const previousState = this.state.websocket;
803
- this.state.websocket = state;
804
- this.state.lastActivity = new Date();
805
- this.updateOverallState();
806
- this.addToHistory();
807
- if (previousState !== state) {
808
- this.emit('websocketStateChange', state, previousState);
809
- }
810
- }
811
- updateWebRTCState(state) {
812
- this.updateWebSocketState(state);
813
- }
814
- updateICEState(state) {
815
- // No-op for backwards compatibility
816
- }
817
- updateOverallState() {
818
- const previousOverall = this.state.overall;
819
- if (this.state.signaling === 'disconnected' || this.state.websocket === 'failed' || this.state.websocket === 'closed') {
820
- this.state.overall = 'disconnected';
821
- }
822
- else if (this.state.signaling === 'connecting' || this.state.websocket === 'connecting') {
823
- this.state.overall = 'connecting';
824
- }
825
- else if (this.state.signaling === 'connected' && this.state.websocket === 'connected') {
826
- this.state.overall = 'connected';
827
- }
828
- else if (this.state.signaling === 'reconnecting' || this.state.websocket === 'disconnected') {
829
- this.state.overall = 'degraded';
830
- }
831
- else if (this.state.errors.length > 0) {
832
- this.state.overall = 'error';
833
- }
834
- else {
835
- this.state.overall = 'connecting';
836
- }
837
- if (previousOverall !== this.state.overall) {
838
- this.emit('overallStateChange', this.state.overall, previousOverall);
839
- }
840
- }
841
- addError(error) {
842
- this.state.errors.push(error);
843
- if (this.state.errors.length > 10) {
844
- this.state.errors = this.state.errors.slice(-10);
845
- }
846
- this.updateOverallState();
847
- this.emit('error', error);
848
- }
849
- clearErrors() {
850
- this.state.errors = [];
851
- this.updateOverallState();
852
- }
853
- updateConnectionQuality(stats) {
854
- const previousScore = this.quality.qualityScore;
855
- if (stats.latency !== undefined)
856
- this.quality.latency = stats.latency;
857
- if (stats.jitter !== undefined)
858
- this.quality.jitter = stats.jitter;
859
- if (stats.packetLoss !== undefined)
860
- this.quality.packetLoss = stats.packetLoss;
861
- if (stats.bandwidth !== undefined)
862
- this.quality.bandwidth = stats.bandwidth;
863
- this.quality.qualityScore = this.calculateQualityScore();
864
- if (Math.abs(previousScore - this.quality.qualityScore) > 0.1) {
865
- this.emit('qualityChange', this.quality);
866
- }
867
- }
868
- calculateQualityScore() {
869
- let score = 1.0;
870
- if (this.quality.latency !== undefined) {
871
- if (this.quality.latency < 150) ;
872
- else if (this.quality.latency < 300) {
873
- score -= 0.1;
874
- }
875
- else if (this.quality.latency < 500) {
876
- score -= 0.3;
877
- }
878
- else {
879
- score -= 0.5;
880
- }
881
- }
882
- if (this.quality.packetLoss !== undefined) {
883
- if (this.quality.packetLoss < 1) ;
884
- else if (this.quality.packetLoss < 3) {
885
- score -= 0.2;
886
- }
887
- else if (this.quality.packetLoss < 5) {
888
- score -= 0.4;
889
- }
890
- else {
891
- score -= 0.6;
892
- }
893
- }
894
- if (this.quality.jitter !== undefined) {
895
- if (this.quality.jitter < 30) ;
896
- else if (this.quality.jitter < 50) {
897
- score -= 0.1;
898
- }
899
- else if (this.quality.jitter < 100) {
900
- score -= 0.2;
901
- }
902
- else {
903
- score -= 0.3;
904
- }
905
- }
906
- return Math.max(0, Math.min(1, score));
907
- }
908
- startHealthCheck(intervalMs = 5000) {
909
- this.stopHealthCheck();
910
- this.healthCheckInterval = setInterval(() => {
911
- const timeSinceLastActivity = Date.now() - this.state.lastActivity.getTime();
912
- if (timeSinceLastActivity > 30000 && this.state.overall === 'connected') {
913
- this.emit('healthCheckFailed', timeSinceLastActivity);
914
- }
915
- }, intervalMs);
916
- }
917
- stopHealthCheck() {
918
- if (this.healthCheckInterval) {
919
- clearInterval(this.healthCheckInterval);
920
- this.healthCheckInterval = undefined;
921
- }
922
- }
923
- addToHistory() {
924
- this.stateHistory.push({
925
- state: { ...this.state },
926
- timestamp: new Date()
927
- });
928
- if (this.stateHistory.length > this.maxHistorySize) {
929
- this.stateHistory = this.stateHistory.slice(-this.maxHistorySize);
930
- }
931
- }
932
- getState() {
933
- return { ...this.state };
934
- }
935
- getQuality() {
936
- return { ...this.quality };
937
- }
938
- getStateHistory() {
939
- return [...this.stateHistory];
940
- }
941
- isConnected() {
942
- return this.state.overall === 'connected';
943
- }
944
- isHealthy() {
945
- return this.state.overall === 'connected' &&
946
- this.quality.qualityScore > 0.5 &&
947
- this.state.errors.length === 0;
948
- }
949
- getReconnectAttempts() {
950
- return this.state.reconnectAttempts;
951
- }
952
- reset() {
953
- this.state = {
954
- signaling: 'disconnected',
955
- websocket: 'new',
956
- overall: 'disconnected',
957
- lastActivity: new Date(),
958
- reconnectAttempts: 0,
959
- errors: []
960
- };
961
- this.quality = {
962
- qualityScore: 1.0
963
- };
964
- this.stateHistory = [];
965
- this.emit('reset');
966
- }
967
- destroy() {
968
- this.stopHealthCheck();
969
- this.removeAllListeners();
970
- }
971
- }
972
-
973
- class PerformanceMonitor extends eventemitter3.EventEmitter {
974
- constructor(config) {
975
- super();
976
- this.benchmarks = [];
977
- this.isCollecting = false;
978
- this.config = {
979
- collectionInterval: 5000,
980
- enableAutoReport: true,
981
- reportingInterval: 30000,
982
- sessionId: '',
983
- ...config
984
- };
985
- this.startTime = Date.now();
986
- this.metrics = this.initializeMetrics();
987
- }
988
- initializeMetrics() {
989
- return {
990
- latency: {
991
- signaling: { avg: 0, min: Infinity, max: 0, samples: [] },
992
- websocket: { avg: 0, min: Infinity, max: 0, samples: [] },
993
- audio: { avg: 0, min: Infinity, max: 0, samples: [] }
994
- },
995
- throughput: {
996
- audio: { inbound: 0, outbound: 0 },
997
- data: { inbound: 0, outbound: 0 }
998
- },
999
- quality: {
1000
- audioQuality: 1.0,
1001
- connectionQuality: 1.0
1002
- },
1003
- resources: {
1004
- cpuUsage: 0,
1005
- memoryUsage: 0,
1006
- bandwidth: { upload: 0, download: 0 }
1007
- }
1008
- };
1009
- }
1010
- startCollection(sessionId) {
1011
- if (this.isCollecting)
1012
- return;
1013
- this.isCollecting = true;
1014
- if (sessionId) {
1015
- this.config.sessionId = sessionId;
1016
- }
1017
- this.collectionTimer = setInterval(() => {
1018
- this.collectMetrics();
1019
- }, this.config.collectionInterval);
1020
- if (this.config.enableAutoReport) {
1021
- this.reportingTimer = setInterval(() => {
1022
- this.reportMetrics();
1023
- }, this.config.reportingInterval);
1024
- }
1025
- this.emit('collectionStarted');
1026
- }
1027
- stopCollection() {
1028
- if (!this.isCollecting)
1029
- return;
1030
- this.isCollecting = false;
1031
- if (this.collectionTimer) {
1032
- clearInterval(this.collectionTimer);
1033
- this.collectionTimer = undefined;
1034
- }
1035
- if (this.reportingTimer) {
1036
- clearInterval(this.reportingTimer);
1037
- this.reportingTimer = undefined;
1038
- }
1039
- if (this.config.enableAutoReport) {
1040
- this.reportMetrics();
1041
- }
1042
- this.emit('collectionStopped');
1043
- }
1044
- recordLatency(type, latency) {
1045
- const key = type === 'webrtc' ? 'websocket' : type === 'video' ? 'audio' : type;
1046
- const metric = this.metrics.latency[key];
1047
- if (!metric)
1048
- return;
1049
- metric.samples.push(latency);
1050
- if (metric.samples.length > 100) {
1051
- metric.samples.shift();
1052
- }
1053
- metric.min = Math.min(metric.min, latency);
1054
- metric.max = Math.max(metric.max, latency);
1055
- metric.avg = metric.samples.reduce((a, b) => a + b, 0) / metric.samples.length;
1056
- }
1057
- updateThroughput(type, direction, bytesPerSecond) {
1058
- const key = type === 'video' ? 'audio' : type;
1059
- if (this.metrics.throughput[key]) {
1060
- this.metrics.throughput[key][direction] = bytesPerSecond;
1061
- }
1062
- }
1063
- updateQuality(quality) {
1064
- this.metrics.quality = { ...this.metrics.quality, ...quality };
1065
- }
1066
- updateResources(resources) {
1067
- this.metrics.resources = { ...this.metrics.resources, ...resources };
1068
- }
1069
- recordBenchmark(benchmark) {
1070
- const fullBenchmark = {
1071
- ...benchmark,
1072
- timestamp: new Date()
1073
- };
1074
- this.benchmarks.push(fullBenchmark);
1075
- if (this.benchmarks.length > 50) {
1076
- this.benchmarks.shift();
1077
- }
1078
- this.emit('benchmarkRecorded', fullBenchmark);
1079
- }
1080
- getMetrics() {
1081
- return JSON.parse(JSON.stringify(this.metrics));
1082
- }
1083
- getBenchmarks() {
1084
- return [...this.benchmarks];
1085
- }
1086
- getLatencyStats(type) {
1087
- if (type) {
1088
- const key = type === 'webrtc' ? 'websocket' : type === 'video' ? 'audio' : type;
1089
- return { ...this.metrics.latency[key] };
1090
- }
1091
- return JSON.parse(JSON.stringify(this.metrics.latency));
1092
- }
1093
- async reportMetrics() {
1094
- if (!this.config.sessionId)
1095
- return;
1096
- try {
1097
- const report = {
1098
- sessionId: this.config.sessionId,
1099
- timestamp: new Date(),
1100
- duration: Date.now() - this.startTime,
1101
- metrics: this.getMetrics(),
1102
- benchmarks: this.getBenchmarks()
1103
- };
1104
- const response = await fetch(`${this.config.apiEndpoint}/performance/report`, {
1105
- method: 'POST',
1106
- headers: {
1107
- 'Content-Type': 'application/json'
1108
- },
1109
- body: JSON.stringify(report)
1110
- });
1111
- if (!response.ok) {
1112
- throw new Error(`Failed to report metrics: ${response.statusText}`);
1113
- }
1114
- this.emit('metricsReported', report);
1115
- }
1116
- catch (error) {
1117
- this.emit('error', error);
1118
- }
1119
- }
1120
- collectMetrics() {
1121
- this.emit('collectMetrics', {
1122
- timestamp: Date.now(),
1123
- metrics: this.metrics
1124
- });
1125
- }
1126
- destroy() {
1127
- this.stopCollection();
1128
- this.removeAllListeners();
1129
- this.benchmarks = [];
1130
- }
1131
- }
1132
-
1133
- class AnalyticsCollector extends eventemitter3.EventEmitter {
1134
- constructor(config) {
1135
- super();
1136
- this.events = [];
1137
- this.sessionAnalytics = null;
1138
- this.isCollecting = false;
1139
- this.config = {
1140
- enableAutoFlush: true,
1141
- flushInterval: 60000, // 1 minute
1142
- batchSize: 100,
1143
- sessionId: '',
1144
- userId: '',
1145
- organizationId: '',
1146
- ...config
1147
- };
1148
- }
1149
- startSession(sessionId) {
1150
- if (this.isCollecting) {
1151
- this.endSession();
1152
- }
1153
- this.isCollecting = true;
1154
- this.config.sessionId = sessionId;
1155
- this.sessionAnalytics = {
1156
- sessionId,
1157
- startTime: new Date(),
1158
- duration: 0,
1159
- eventsCount: 0,
1160
- errorCount: 0,
1161
- mediaTypes: [],
1162
- connectionQuality: 1.0,
1163
- dataTransferred: {
1164
- audio: { sent: 0, received: 0 },
1165
- video: { sent: 0, received: 0 },
1166
- text: { sent: 0, received: 0 }
1167
- }
1168
- };
1169
- if (this.config.enableAutoFlush) {
1170
- this.flushTimer = setInterval(() => {
1171
- this.flush();
1172
- }, this.config.flushInterval);
1173
- }
1174
- this.trackEvent('session_started', {});
1175
- }
1176
- endSession() {
1177
- if (!this.isCollecting || !this.sessionAnalytics)
1178
- return;
1179
- this.isCollecting = false;
1180
- this.sessionAnalytics.endTime = new Date();
1181
- this.sessionAnalytics.duration =
1182
- (this.sessionAnalytics.endTime.getTime() - this.sessionAnalytics.startTime.getTime()) / 1000;
1183
- this.trackEvent('session_ended', {
1184
- duration: this.sessionAnalytics.duration
1185
- });
1186
- this.flush();
1187
- if (this.flushTimer) {
1188
- clearInterval(this.flushTimer);
1189
- this.flushTimer = undefined;
1190
- }
1191
- this.sendSessionAnalytics();
1192
- }
1193
- trackEvent(eventType, metadata = {}) {
1194
- const event = {
1195
- eventType,
1196
- timestamp: new Date(),
1197
- sessionId: this.config.sessionId,
1198
- userId: this.config.userId,
1199
- organizationId: this.config.organizationId,
1200
- metadata
1201
- };
1202
- this.events.push(event);
1203
- if (this.sessionAnalytics) {
1204
- this.sessionAnalytics.eventsCount++;
1205
- if (eventType.includes('error') || metadata.error) {
1206
- this.sessionAnalytics.errorCount++;
1207
- }
1208
- }
1209
- if (this.events.length >= this.config.batchSize) {
1210
- this.flush();
1211
- }
1212
- this.emit('eventTracked', event);
1213
- }
1214
- updateMediaTypes(mediaTypes) {
1215
- if (this.sessionAnalytics) {
1216
- this.sessionAnalytics.mediaTypes = [...new Set(mediaTypes)];
1217
- }
1218
- }
1219
- updateConnectionQuality(quality) {
1220
- if (this.sessionAnalytics) {
1221
- const weight = 0.1;
1222
- this.sessionAnalytics.connectionQuality =
1223
- this.sessionAnalytics.connectionQuality * (1 - weight) + quality * weight;
1224
- }
1225
- }
1226
- updateDataTransferred(type, direction, bytes) {
1227
- if (this.sessionAnalytics) {
1228
- this.sessionAnalytics.dataTransferred[type][direction] += bytes;
1229
- }
1230
- }
1231
- getSessionAnalytics() {
1232
- if (!this.sessionAnalytics)
1233
- return null;
1234
- return {
1235
- ...this.sessionAnalytics,
1236
- duration: this.isCollecting
1237
- ? (Date.now() - this.sessionAnalytics.startTime.getTime()) / 1000
1238
- : this.sessionAnalytics.duration
1239
- };
1240
- }
1241
- getEvents() {
1242
- return [...this.events];
1243
- }
1244
- async flush() {
1245
- if (this.events.length === 0)
1246
- return;
1247
- const eventsToSend = [...this.events];
1248
- this.events = [];
1249
- try {
1250
- const response = await fetch(`${this.config.apiEndpoint}/analytics/events`, {
1251
- method: 'POST',
1252
- headers: {
1253
- 'Content-Type': 'application/json',
1254
- 'Authorization': `Bearer ${this.config.apiKey}`
1255
- },
1256
- body: JSON.stringify({
1257
- events: eventsToSend
1258
- })
1259
- });
1260
- if (!response.ok) {
1261
- throw new Error(`Failed to send analytics: ${response.statusText}`);
1262
- }
1263
- this.emit('eventsFlushed', eventsToSend.length);
1264
- }
1265
- catch (error) {
1266
- this.events.unshift(...eventsToSend);
1267
- this.emit('error', error);
1268
- }
1269
- }
1270
- async sendSessionAnalytics() {
1271
- if (!this.sessionAnalytics)
1272
- return;
1273
- try {
1274
- const response = await fetch(`${this.config.apiEndpoint}/analytics/sessions`, {
1275
- method: 'POST',
1276
- headers: {
1277
- 'Content-Type': 'application/json',
1278
- 'Authorization': `Bearer ${this.config.apiKey}`
1279
- },
1280
- body: JSON.stringify(this.sessionAnalytics)
1281
- });
1282
- if (!response.ok) {
1283
- throw new Error(`Failed to send session analytics: ${response.statusText}`);
1284
- }
1285
- this.emit('sessionAnalyticsSent', this.sessionAnalytics);
1286
- }
1287
- catch (error) {
1288
- this.emit('error', error);
1289
- }
1290
- }
1291
- destroy() {
1292
- if (this.isCollecting) {
1293
- this.endSession();
1294
- }
1295
- if (this.flushTimer) {
1296
- clearInterval(this.flushTimer);
1297
- this.flushTimer = undefined;
1298
- }
1299
- this.removeAllListeners();
1300
- this.events = [];
1301
- this.sessionAnalytics = null;
1302
- }
1303
- }
1304
-
1305
- class UsageTracker {
1306
- constructor(sessionId) {
1307
- this.inputTokens = 0;
1308
- this.outputTokens = 0;
1309
- this.errorCount = 0;
1310
- this.qualityScores = [];
1311
- this.sessionId = sessionId;
1312
- this.startTime = new Date();
1313
- }
1314
- start() {
1315
- this.startTime = new Date();
1316
- }
1317
- stop() {
1318
- this.endTime = new Date();
1319
- }
1320
- trackInputTokens(tokens) {
1321
- this.inputTokens += tokens;
1322
- }
1323
- trackOutputTokens(tokens) {
1324
- this.outputTokens += tokens;
1325
- }
1326
- trackError() {
1327
- this.errorCount++;
1328
- }
1329
- trackQualityScore(score) {
1330
- if (score >= 0 && score <= 1) {
1331
- this.qualityScores.push(score);
1332
- }
1333
- }
1334
- getDuration() {
1335
- const end = this.endTime || new Date();
1336
- return Math.floor((end.getTime() - this.startTime.getTime()) / 1000);
1337
- }
1338
- getAverageQualityScore() {
1339
- if (this.qualityScores.length === 0)
1340
- return undefined;
1341
- const sum = this.qualityScores.reduce((acc, score) => acc + score, 0);
1342
- return sum / this.qualityScores.length;
1343
- }
1344
- getMetrics() {
1345
- return {
1346
- sessionId: this.sessionId,
1347
- duration: this.getDuration(),
1348
- inputTokens: this.inputTokens > 0 ? this.inputTokens : undefined,
1349
- outputTokens: this.outputTokens > 0 ? this.outputTokens : undefined,
1350
- errorCount: this.errorCount,
1351
- qualityScore: this.getAverageQualityScore()
1352
- };
1353
- }
1354
- reset() {
1355
- this.startTime = new Date();
1356
- this.endTime = undefined;
1357
- this.inputTokens = 0;
1358
- this.outputTokens = 0;
1359
- this.errorCount = 0;
1360
- this.qualityScores = [];
1361
- }
1362
- }
1363
-
1364
- /**
1365
- * @deprecated This module is deprecated. Use AStackCSRClient for WebSocket-based communication.
1366
- * This file is kept for backwards compatibility only.
1367
- */
1368
- /**
1369
- * @deprecated Use AStackCSRClient instead. This function is kept for backwards compatibility.
1370
- */
1371
- function setupEventHandlers(emitter, deps) {
1372
- console.warn('setupEventHandlers is deprecated. Use AStackCSRClient for WebSocket-based communication.');
1373
- const { signaling, connectionStateManager, securityLogger } = deps;
1374
- signaling.on('sessionJoined', () => {
1375
- const sessionId = deps.getSessionId();
1376
- const usageTracker = new UsageTracker(sessionId);
1377
- usageTracker.start();
1378
- deps.setUsageTracker(usageTracker);
1379
- securityLogger.setSessionId(sessionId);
1380
- connectionStateManager.updateSignalingState('connected');
1381
- connectionStateManager.startHealthCheck();
1382
- emitter.emit('sessionReady', sessionId);
1383
- });
1384
- signaling.on('aiResponse', (data) => {
1385
- const response = {
1386
- type: 'text',
1387
- content: data.text || data.message,
1388
- timestamp: data.timestamp || Date.now(),
1389
- messageId: data.messageId
1390
- };
1391
- emitter.emit('messageReceived', response);
1392
- });
1393
- signaling.on('audioResponse', (data) => {
1394
- if (data.audio) {
1395
- try {
1396
- const binaryString = atob(data.audio.split(',')[1]);
1397
- const bytes = new Uint8Array(binaryString.length);
1398
- for (let i = 0; i < binaryString.length; i++) {
1399
- bytes[i] = binaryString.charCodeAt(i);
1400
- }
1401
- const audioBlob = new Blob([bytes], { type: 'audio/wav' });
1402
- const response = {
1403
- type: 'audio',
1404
- content: audioBlob,
1405
- timestamp: data.timestamp || Date.now(),
1406
- messageId: data.messageId
1407
- };
1408
- emitter.emit('messageReceived', response);
1409
- }
1410
- catch (error) { }
1411
- }
1412
- });
1413
- signaling.on('textResponse', (data) => {
1414
- emitter.emit('text-response', data);
1415
- });
1416
- signaling.on('disconnected', () => {
1417
- deps.setStatus({ status: 'disconnected' });
1418
- connectionStateManager.updateSignalingState('disconnected');
1419
- emitter.emit('disconnected');
1420
- });
1421
- signaling.on('reconnecting', (attempt) => {
1422
- connectionStateManager.updateSignalingState('reconnecting');
1423
- emitter.emit('reconnecting', attempt);
1424
- });
1425
- signaling.on('error', (error) => {
1426
- const usageTracker = deps.getUsageTracker();
1427
- if (usageTracker) {
1428
- usageTracker.trackError();
1429
- }
1430
- connectionStateManager.addError(error);
1431
- if (error.code === ErrorCodes.AUTHENTICATION_FAILED || error.code === ErrorCodes.INVALID_API_KEY) {
1432
- securityLogger.logAuthFailure(error.message, { code: error.code });
1433
- }
1434
- else if (error.code === ErrorCodes.RATE_LIMIT_EXCEEDED) {
1435
- securityLogger.logRateLimit(0, 0, 'signaling');
1436
- }
1437
- emitter.emit('error', error);
1438
- });
1439
- }
1440
-
1441
- class AStackMediaController {
1442
- constructor(webrtc, signaling, analyticsCollector, config, emitter) {
1443
- this.audioRecorder = null;
1444
- this.isRecording = false;
1445
- this.webrtc = webrtc;
1446
- this.signaling = signaling;
1447
- this.analyticsCollector = analyticsCollector;
1448
- this.config = config;
1449
- this.emitter = emitter;
1450
- }
1451
- async startVideo() {
1452
- if (!this.config.enableVideo) {
1453
- throw new AStackError('Video is disabled in configuration', ErrorCodes.VIDEO_ERROR);
1454
- }
1455
- try {
1456
- const stream = await this.webrtc.getUserMedia();
1457
- this.webrtc.addLocalStream(stream);
1458
- this.emitter.emit('videoStarted');
1459
- this.analyticsCollector.trackEvent('video_started');
1460
- this.analyticsCollector.updateMediaTypes(['video']);
1461
- return stream;
1462
- }
1463
- catch (error) {
1464
- throw new AStackError(`Failed to start video: ${error}`, ErrorCodes.VIDEO_ERROR);
1465
- }
1466
- }
1467
- async stopVideo(localVideoRef) {
1468
- const localStreams = this.webrtc.getLocalStreams();
1469
- localStreams.forEach(stream => {
1470
- if (stream.kind === 'video') {
1471
- this.webrtc.removeLocalStream(stream.id);
1472
- }
1473
- });
1474
- if (localVideoRef) {
1475
- localVideoRef.srcObject = null;
1476
- }
1477
- this.emitter.emit('videoStopped');
1478
- }
1479
- async startAudio() {
1480
- if (this.config.enableAudio === false) {
1481
- throw new AStackError('Audio is disabled in configuration', ErrorCodes.AUDIO_ERROR);
1482
- }
1483
- try {
1484
- const stream = await this.webrtc.getUserMedia();
1485
- this.webrtc.addLocalStream(stream);
1486
- this.emitter.emit('audioStarted');
1487
- this.analyticsCollector.trackEvent('audio_started');
1488
- this.analyticsCollector.updateMediaTypes(['audio']);
1489
- return stream;
1490
- }
1491
- catch (error) {
1492
- throw new AStackError(`Failed to start audio: ${error}`, ErrorCodes.AUDIO_ERROR);
1493
- }
1494
- }
1495
- async stopAudio() {
1496
- const localStreams = this.webrtc.getLocalStreams();
1497
- localStreams.forEach(stream => {
1498
- if (stream.kind === 'audio') {
1499
- this.webrtc.removeLocalStream(stream.id);
1500
- }
1501
- });
1502
- if (this.isRecording) {
1503
- this.stopRecording();
1504
- }
1505
- this.emitter.emit('audioStopped');
1506
- }
1507
- async sendText(message) {
1508
- if (!this.signaling.isConnected()) {
1509
- throw new AStackError('Not connected to AStack', ErrorCodes.CONNECTION_LOST);
1510
- }
1511
- try {
1512
- this.signaling.sendTextMessage(message);
1513
- this.emitter.emit('messageSent', message);
1514
- }
1515
- catch (error) {
1516
- throw new AStackError(`Failed to send text message: ${error}`, ErrorCodes.SIGNALING_ERROR);
1517
- }
1518
- }
1519
- async sendAudio(audioBlob) {
1520
- if (!this.signaling.isConnected()) {
1521
- throw new AStackError('Not connected to AStack', ErrorCodes.CONNECTION_LOST);
1522
- }
1523
- try {
1524
- this.signaling.sendAudioData(audioBlob);
1525
- this.emitter.emit('messageSent', audioBlob);
1526
- }
1527
- catch (error) {
1528
- throw new AStackError(`Failed to send audio: ${error}`, ErrorCodes.AUDIO_ERROR);
1529
- }
1530
- }
1531
- startRecording() {
1532
- const localStreams = this.webrtc.getLocalStreams();
1533
- const audioStream = localStreams.find(s => s.kind === 'audio');
1534
- if (!audioStream?.track) {
1535
- throw new AStackError('No audio stream available for recording', ErrorCodes.AUDIO_ERROR);
1536
- }
1537
- const stream = new MediaStream([audioStream.track]);
1538
- this.audioRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
1539
- const audioChunks = [];
1540
- this.audioRecorder.ondataavailable = (event) => {
1541
- if (event.data.size > 0) {
1542
- audioChunks.push(event.data);
1543
- }
1544
- };
1545
- this.audioRecorder.onstop = () => {
1546
- const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
1547
- this.sendAudio(audioBlob);
1548
- };
1549
- this.audioRecorder.start();
1550
- this.isRecording = true;
1551
- }
1552
- stopRecording() {
1553
- if (this.audioRecorder && this.isRecording) {
1554
- this.audioRecorder.stop();
1555
- this.isRecording = false;
1556
- }
1557
- }
1558
- getIsRecording() {
1559
- return this.isRecording;
1560
- }
1561
- async getDevices() {
1562
- return this.webrtc.getMediaDevices();
1563
- }
1564
- async switchCamera(deviceId) {
1565
- throw new AStackError('Camera switching not yet implemented', ErrorCodes.VIDEO_ERROR);
1566
- }
1567
- async switchMicrophone(deviceId) {
1568
- return this.webrtc.switchAudioInput(deviceId);
1569
- }
1570
- muteAudio() {
1571
- this.webrtc.muteAudio();
1572
- }
1573
- unmuteAudio() {
1574
- this.webrtc.unmuteAudio();
1575
- }
1576
- pauseVideo() {
1577
- this.webrtc.muteVideo();
1578
- }
1579
- resumeVideo() {
1580
- this.webrtc.unmuteVideo();
1581
- }
1582
- getLocalStreams() {
1583
- return this.webrtc.getLocalStreams();
1584
- }
1585
- getRemoteStreams() {
1586
- return this.webrtc.getRemoteStreams();
1587
- }
1588
- }
1589
-
1590
- /**
1591
- * @deprecated AStackClient is deprecated. Use AStackCSRClient for WebSocket-based communication.
1592
- * This class is kept for backwards compatibility only.
1593
- *
1594
- * Example:
1595
- * ```typescript
1596
- * import { AStackCSRClient } from '@astack/client-sdk';
1597
- * const client = new AStackCSRClient({ apiEndpoint: 'https://api.astack.com' });
1598
- * ```
1599
- */
1600
- class AStackClient extends eventemitter3.EventEmitter {
1601
- constructor(config) {
1602
- super();
1603
- this.usageTracker = null;
1604
- this.sessionId = null;
1605
- this.localVideoRef = null;
1606
- this.remoteVideoRef = null;
1607
- if (!config.apiKey) {
1608
- throw new AStackError('API key is required', ErrorCodes.INVALID_API_KEY);
1609
- }
1610
- if (!config.supabaseUrl || !config.supabaseAnonKey) {
1611
- throw new AStackError('Supabase URL and Anon Key are required', ErrorCodes.AUTHENTICATION_FAILED);
1612
- }
1613
- this.config = {
1614
- enableVideo: false, enableAudio: true, enableText: true, autoReconnect: true,
1615
- maxRetries: 3, reconnectDelay: 1000, heartbeatInterval: 30000, connectionTimeout: 10000,
1616
- sessionExpirationWarning: 1, apiEndpoint: `${config.supabaseUrl}/functions/v1`, ...config
1617
- };
1618
- this.webrtc = new WebRTCManager(this.config);
1619
- this.signaling = new SupabaseSignalingClient(this.config);
1620
- this.sessionManager = new SessionManager(this.config.apiEndpoint, this.config.sessionExpirationWarning, (minutesRemaining) => this.emit('sessionExpiring', minutesRemaining));
1621
- this.billingMonitor = new BillingMonitor(this.config.apiEndpoint, this.config.apiKey);
1622
- this.billingMonitor.onAlert((alert) => this.emit('billingAlert', alert));
1623
- this.securityLogger = new SecurityLogger({
1624
- apiEndpoint: this.config.apiEndpoint, apiKey: this.config.apiKey,
1625
- organizationId: this.config.organizationId, enableLocalLogging: true
1626
- });
1627
- this.connectionStateManager = new ConnectionStateManager();
1628
- this.connectionStateManager.on('qualityChange', (quality) => {
1629
- this.analyticsCollector.updateConnectionQuality(quality.qualityScore);
1630
- });
1631
- this.performanceMonitor = new PerformanceMonitor({
1632
- apiEndpoint: this.config.apiEndpoint,
1633
- enableAutoReport: this.config.enablePerformanceReporting ?? true,
1634
- collectionInterval: this.config.performanceCollectionInterval ?? 5000,
1635
- reportingInterval: this.config.performanceReportingInterval ?? 30000
1636
- });
1637
- this.analyticsCollector = new AnalyticsCollector({
1638
- apiEndpoint: this.config.apiEndpoint, apiKey: this.config.apiKey,
1639
- organizationId: this.config.organizationId, userId: this.config.endUserId,
1640
- enableAutoFlush: this.config.enableAnalytics ?? true
1641
- });
1642
- this.mediaController = new AStackMediaController(this.webrtc, this.signaling, this.analyticsCollector, this.config, this);
1643
- this.status = { id: '', status: 'pending', connectionState: 'disconnected', iceConnectionState: 'new' };
1644
- this.initEventHandlers();
1645
- }
1646
- initEventHandlers() {
1647
- setupEventHandlers(this, {
1648
- webrtc: this.webrtc, signaling: this.signaling,
1649
- connectionStateManager: this.connectionStateManager, analyticsCollector: this.analyticsCollector,
1650
- securityLogger: this.securityLogger, performanceMonitor: this.performanceMonitor,
1651
- getUsageTracker: () => this.usageTracker, setUsageTracker: (t) => { this.usageTracker = t; },
1652
- getSessionId: () => this.sessionId, getStatus: () => this.status,
1653
- setStatus: (s) => { this.status = { ...this.status, ...s }; },
1654
- localVideoRef: this.localVideoRef, remoteVideoRef: this.remoteVideoRef,
1655
- startWebRTCStatsCollection: () => this.startWebRTCStatsCollection(),
1656
- stopWebRTCStatsCollection: () => this.stopWebRTCStatsCollection()
1657
- });
1658
- }
1659
- startWebRTCStatsCollection() {
1660
- this.stopWebRTCStatsCollection();
1661
- this.statsCollectionTimer = setInterval(async () => {
1662
- try {
1663
- const stats = await this.webrtc.getStats();
1664
- if (stats) {
1665
- const processed = this.processWebRTCStats(stats);
1666
- if (processed.audioLatency !== undefined)
1667
- this.performanceMonitor.recordLatency('audio', processed.audioLatency);
1668
- if (processed.videoLatency !== undefined)
1669
- this.performanceMonitor.recordLatency('video', processed.videoLatency);
1670
- if (processed.throughput) {
1671
- this.performanceMonitor.updateThroughput('audio', 'inbound', processed.throughput.audio.inbound);
1672
- this.performanceMonitor.updateThroughput('audio', 'outbound', processed.throughput.audio.outbound);
1673
- this.performanceMonitor.updateThroughput('video', 'inbound', processed.throughput.video.inbound);
1674
- this.performanceMonitor.updateThroughput('video', 'outbound', processed.throughput.video.outbound);
1675
- }
1676
- if (processed.quality)
1677
- this.performanceMonitor.updateQuality(processed.quality);
1678
- }
1679
- }
1680
- catch (error) { }
1681
- }, 5000);
1682
- }
1683
- stopWebRTCStatsCollection() {
1684
- if (this.statsCollectionTimer) {
1685
- clearInterval(this.statsCollectionTimer);
1686
- this.statsCollectionTimer = undefined;
1687
- }
1688
- }
1689
- processWebRTCStats(stats) {
1690
- return { throughput: { audio: { inbound: 0, outbound: 0 }, video: { inbound: 0, outbound: 0 } }, quality: {} };
1691
- }
1692
- async connect() {
1693
- try {
1694
- this.connectionStateManager.updateSignalingState('connecting');
1695
- const sessionRequest = {
1696
- apiKey: this.config.apiKey, organizationId: this.config.organizationId,
1697
- endUserId: this.config.endUserId,
1698
- metadata: { enableVideo: this.config.enableVideo, enableAudio: this.config.enableAudio },
1699
- workerPreferences: this.config.workerPreferences
1700
- };
1701
- const credentials = await this.sessionManager.createSession(sessionRequest);
1702
- this.sessionId = credentials.sessionId;
1703
- this.status = {
1704
- id: this.sessionId, status: 'connecting', connectionState: 'disconnected',
1705
- iceConnectionState: 'new', startTime: new Date()
1706
- };
1707
- this.billingMonitor.startMonitoring();
1708
- this.performanceMonitor.startCollection(this.sessionId);
1709
- this.analyticsCollector.startSession(this.sessionId);
1710
- await this.webrtc.initialize();
1711
- await this.signaling.connect(credentials.sessionId, credentials.channelName, credentials.wsToken);
1712
- }
1713
- catch (error) {
1714
- this.status.status = 'error';
1715
- this.status.lastError = error;
1716
- if (error instanceof AStackError) {
1717
- if (error.code === ErrorCodes.INSUFFICIENT_CREDITS) {
1718
- this.emit('billingAlert', { type: 'limit_exceeded', details: { error: error.message } });
1719
- }
1720
- else if (error.code === ErrorCodes.RATE_LIMIT_EXCEEDED) {
1721
- this.emit('rateLimitWarning', { limit: 0, current: 0, resetIn: 60 });
1722
- }
1723
- }
1724
- throw error;
1725
- }
1726
- }
1727
- async startVideo() { return this.mediaController.startVideo(); }
1728
- async stopVideo() { return this.mediaController.stopVideo(this.localVideoRef); }
1729
- async startAudio() { return this.mediaController.startAudio(); }
1730
- async stopAudio() { return this.mediaController.stopAudio(); }
1731
- async sendText(message) { return this.mediaController.sendText(message); }
1732
- async sendAudio(audioBlob) { return this.mediaController.sendAudio(audioBlob); }
1733
- startRecording() { this.mediaController.startRecording(); }
1734
- stopRecording() { this.mediaController.stopRecording(); }
1735
- async getDevices() { return this.mediaController.getDevices(); }
1736
- async switchCamera(deviceId) { return this.mediaController.switchCamera(deviceId); }
1737
- async switchMicrophone(deviceId) { return this.mediaController.switchMicrophone(deviceId); }
1738
- muteAudio() { this.mediaController.muteAudio(); }
1739
- unmuteAudio() { this.mediaController.unmuteAudio(); }
1740
- pauseVideo() { this.mediaController.pauseVideo(); }
1741
- resumeVideo() { this.mediaController.resumeVideo(); }
1742
- getSessionStatus() { return { ...this.status }; }
1743
- getLocalStreams() { return this.mediaController.getLocalStreams(); }
1744
- getRemoteStreams() { return this.mediaController.getRemoteStreams(); }
1745
- attachVideo(localVideo, remoteVideo) {
1746
- this.localVideoRef = localVideo;
1747
- this.remoteVideoRef = remoteVideo;
1748
- }
1749
- addEventListener(event, listener) {
1750
- return super.on(event, listener);
1751
- }
1752
- removeEventListener(event, listener) {
1753
- return super.off(event, listener);
1754
- }
1755
- async disconnect() {
1756
- this.status.status = 'disconnected';
1757
- if (this.mediaController.getIsRecording())
1758
- this.mediaController.stopRecording();
1759
- if (this.usageTracker) {
1760
- this.usageTracker.stop();
1761
- this.usageTracker.getMetrics();
1762
- }
1763
- this.billingMonitor.stopMonitoring();
1764
- this.performanceMonitor.stopCollection();
1765
- this.analyticsCollector.endSession();
1766
- this.connectionStateManager.stopHealthCheck();
1767
- this.connectionStateManager.updateSignalingState('disconnected');
1768
- await this.webrtc.close();
1769
- await this.signaling.disconnect();
1770
- this.sessionManager.clearSession();
1771
- if (this.localVideoRef)
1772
- this.localVideoRef.srcObject = null;
1773
- if (this.remoteVideoRef)
1774
- this.remoteVideoRef.srcObject = null;
1775
- this.emit('sessionEnded', this.sessionId);
1776
- this.sessionId = null;
1777
- this.usageTracker = null;
1778
- }
1779
- async renewSession() {
1780
- if (!this.sessionManager.isSessionValid()) {
1781
- throw new AStackError('No active session to renew', ErrorCodes.SESSION_EXPIRED);
1782
- }
1783
- try {
1784
- const credentials = await this.sessionManager.renewSession(this.config.apiKey);
1785
- await this.signaling.disconnect();
1786
- await this.signaling.connect(credentials.sessionId, credentials.channelName, credentials.wsToken);
1787
- this.sessionId = credentials.sessionId;
1788
- this.emit('sessionReady', this.sessionId);
1789
- }
1790
- catch (error) {
1791
- this.emit('error', error);
1792
- throw error;
1793
- }
1794
- }
1795
- getSessionTimeRemaining() { return this.sessionManager.getTimeUntilExpiration(); }
1796
- isSessionValid() { return this.sessionManager.isSessionValid(); }
1797
- getUsageMetrics() { return this.usageTracker?.getMetrics() || null; }
1798
- trackQualityScore(score) { this.usageTracker?.trackQualityScore(score); }
1799
- async getBillingInfo() { return this.billingMonitor.fetchBillingInfo(); }
1800
- getCurrentBillingInfo() { return this.billingMonitor.getCurrentBillingInfo(); }
1801
- setBillingWarningThresholds(thresholds) {
1802
- this.billingMonitor.setWarningThresholds(thresholds);
1803
- }
1804
- getConnectionState() { return this.connectionStateManager.getState(); }
1805
- getConnectionQuality() { return this.connectionStateManager.getQuality(); }
1806
- isHealthy() { return this.connectionStateManager.isHealthy(); }
1807
- getConnectionHistory() {
1808
- return this.connectionStateManager.getStateHistory();
1809
- }
1810
- getPerformanceMetrics() { return this.performanceMonitor.getMetrics(); }
1811
- getPerformanceBenchmarks() { return this.performanceMonitor.getBenchmarks(); }
1812
- recordPerformanceBenchmark(benchmark) {
1813
- this.performanceMonitor.recordBenchmark(benchmark);
1814
- }
1815
- recordLatency(type, latency) {
1816
- this.performanceMonitor.recordLatency(type, latency);
1817
- }
1818
- async reportPerformanceMetrics() { await this.performanceMonitor.reportMetrics(); }
1819
- setWorkerPreferences(preferences) { this.config.workerPreferences = preferences; }
1820
- getWorkerPreferences() { return this.config.workerPreferences; }
1821
- getAssignedWorkerInfo() { return this.sessionManager.getWorkerInfo() || undefined; }
1822
- trackEvent(eventType, metadata) {
1823
- this.analyticsCollector.trackEvent(eventType, metadata);
1824
- }
1825
- getSessionAnalytics() { return this.analyticsCollector.getSessionAnalytics(); }
1826
- async flushAnalytics() { await this.analyticsCollector.flush(); }
1827
- async destroy() {
1828
- await this.disconnect();
1829
- this.sessionManager.destroy();
1830
- this.billingMonitor.destroy();
1831
- this.connectionStateManager.destroy();
1832
- this.performanceMonitor.destroy();
1833
- this.analyticsCollector.destroy();
1834
- await this.securityLogger.destroy();
1835
- this.removeAllListeners();
1836
- }
1837
- }
1838
-
1839
- function useAStack(options) {
1840
- const clientRef = react.useRef(null);
1841
- const [isConnected, setIsConnected] = react.useState(false);
1842
- const [isConnecting, setIsConnecting] = react.useState(false);
1843
- const [sessionStatus, setSessionStatus] = react.useState({
1844
- id: '',
1845
- status: 'pending',
1846
- connectionState: 'disconnected',
1847
- iceConnectionState: 'new'
1848
- });
1849
- const [error, setError] = react.useState(null);
1850
- const [localStream, setLocalStream] = react.useState(null);
1851
- const [remoteStream, setRemoteStream] = react.useState(null);
1852
- const [messages, setMessages] = react.useState([]);
1853
- react.useEffect(() => {
1854
- if (!clientRef.current) {
1855
- const client = new AStackClient(options);
1856
- clientRef.current = client;
1857
- client.on('connected', () => {
1858
- setIsConnected(true);
1859
- setIsConnecting(false);
1860
- setError(null);
1861
- });
1862
- client.on('disconnected', () => {
1863
- setIsConnected(false);
1864
- setIsConnecting(false);
1865
- });
1866
- client.on('error', (err) => {
1867
- setError(err);
1868
- setIsConnecting(false);
1869
- });
1870
- client.on('sessionReady', (sessionId) => {
1871
- setSessionStatus(prev => ({ ...prev, id: sessionId, status: 'connected' }));
1872
- });
1873
- client.on('connectionStateChange', (state) => {
1874
- setSessionStatus(prev => ({ ...prev, connectionState: state }));
1875
- });
1876
- client.on('iceConnectionStateChange', (state) => {
1877
- setSessionStatus(prev => ({ ...prev, iceConnectionState: state }));
1878
- });
1879
- client.on('localStreamAdded', (stream) => {
1880
- setLocalStream(stream);
1881
- });
1882
- client.on('remoteStreamAdded', (stream) => {
1883
- setRemoteStream(stream);
1884
- });
1885
- client.on('messageReceived', (response) => {
1886
- setMessages(prev => [...prev, response]);
1887
- });
1888
- client.on('reconnecting', (attempt) => {
1889
- setIsConnecting(true);
1890
- setError(null);
1891
- });
1892
- }
1893
- return () => {
1894
- if (clientRef.current) {
1895
- clientRef.current.disconnect();
1896
- clientRef.current = null;
1897
- }
1898
- };
1899
- }, []);
1900
- react.useEffect(() => {
1901
- if (options.autoConnect && clientRef.current && !isConnected && !isConnecting) {
1902
- connect();
1903
- }
1904
- }, [options.autoConnect]);
1905
- react.useEffect(() => {
1906
- if (isConnected && clientRef.current) {
1907
- if (options.autoStartAudio) {
1908
- startAudio();
1909
- }
1910
- if (options.autoStartVideo) {
1911
- startVideo();
1912
- }
1913
- }
1914
- }, [isConnected, options.autoStartAudio, options.autoStartVideo]);
1915
- const connect = react.useCallback(async () => {
1916
- if (!clientRef.current || isConnected || isConnecting)
1917
- return;
1918
- setIsConnecting(true);
1919
- setError(null);
1920
- try {
1921
- await clientRef.current.connect();
1922
- }
1923
- catch (err) {
1924
- setError(err);
1925
- setIsConnecting(false);
1926
- }
1927
- }, [isConnected, isConnecting]);
1928
- const disconnect = react.useCallback(async () => {
1929
- if (!clientRef.current)
1930
- return;
1931
- try {
1932
- await clientRef.current.disconnect();
1933
- setIsConnected(false);
1934
- setIsConnecting(false);
1935
- setLocalStream(null);
1936
- setRemoteStream(null);
1937
- setError(null);
1938
- }
1939
- catch (err) {
1940
- setError(err);
1941
- }
1942
- }, []);
1943
- const sendMessage = react.useCallback(async (text) => {
1944
- if (!clientRef.current)
1945
- throw new Error('Client not initialized');
1946
- await clientRef.current.sendText(text);
1947
- }, []);
1948
- const sendAudio = react.useCallback(async (audioBlob) => {
1949
- if (!clientRef.current)
1950
- throw new Error('Client not initialized');
1951
- await clientRef.current.sendAudio(audioBlob);
1952
- }, []);
1953
- const startAudio = react.useCallback(async () => {
1954
- if (!clientRef.current)
1955
- throw new Error('Client not initialized');
1956
- try {
1957
- const stream = await clientRef.current.startAudio();
1958
- setLocalStream(stream);
1959
- }
1960
- catch (err) {
1961
- setError(err);
1962
- }
1963
- }, []);
1964
- const stopAudio = react.useCallback(async () => {
1965
- if (!clientRef.current)
1966
- return;
1967
- try {
1968
- await clientRef.current.stopAudio();
1969
- }
1970
- catch (err) {
1971
- setError(err);
1972
- }
1973
- }, []);
1974
- const startVideo = react.useCallback(async () => {
1975
- if (!clientRef.current)
1976
- throw new Error('Client not initialized');
1977
- try {
1978
- const stream = await clientRef.current.startVideo();
1979
- setLocalStream(stream);
1980
- }
1981
- catch (err) {
1982
- setError(err);
1983
- }
1984
- }, []);
1985
- const stopVideo = react.useCallback(async () => {
1986
- if (!clientRef.current)
1987
- return;
1988
- try {
1989
- await clientRef.current.stopVideo();
1990
- }
1991
- catch (err) {
1992
- setError(err);
1993
- }
1994
- }, []);
1995
- const startRecording = react.useCallback(() => {
1996
- if (!clientRef.current)
1997
- throw new Error('Client not initialized');
1998
- clientRef.current.startRecording();
1999
- }, []);
2000
- const stopRecording = react.useCallback(() => {
2001
- if (!clientRef.current)
2002
- return;
2003
- clientRef.current.stopRecording();
2004
- }, []);
2005
- const muteAudio = react.useCallback(() => {
2006
- if (!clientRef.current)
2007
- return;
2008
- clientRef.current.muteAudio();
2009
- }, []);
2010
- const unmuteAudio = react.useCallback(() => {
2011
- if (!clientRef.current)
2012
- return;
2013
- clientRef.current.unmuteAudio();
2014
- }, []);
2015
- const pauseVideo = react.useCallback(() => {
2016
- if (!clientRef.current)
2017
- return;
2018
- clientRef.current.pauseVideo();
2019
- }, []);
2020
- const resumeVideo = react.useCallback(() => {
2021
- if (!clientRef.current)
2022
- return;
2023
- clientRef.current.resumeVideo();
2024
- }, []);
2025
- const clearMessages = react.useCallback(() => {
2026
- setMessages([]);
2027
- }, []);
2028
- return {
2029
- client: clientRef.current,
2030
- isConnected,
2031
- isConnecting,
2032
- sessionStatus,
2033
- error,
2034
- localStream,
2035
- remoteStream,
2036
- messages,
2037
- connect,
2038
- disconnect,
2039
- sendMessage,
2040
- sendAudio,
2041
- startAudio,
2042
- stopAudio,
2043
- startVideo,
2044
- stopVideo,
2045
- startRecording,
2046
- stopRecording,
2047
- muteAudio,
2048
- unmuteAudio,
2049
- pauseVideo,
2050
- resumeVideo,
2051
- clearMessages
2052
- };
2053
- }
2054
-
2055
29
  const ARKIT_BLENDSHAPES = [
2056
30
  'jawOpen', 'jawForward', 'jawLeft', 'jawRight',
2057
31
  'mouthClose', 'mouthFunnel', 'mouthPucker', 'mouthLeft', 'mouthRight',
@@ -2071,12 +45,17 @@ const ARKIT_BLENDSHAPES = [
2071
45
  ];
2072
46
  const BLENDSHAPE_COUNT = 52;
2073
47
 
48
+ const IDX_JAW_OPEN = ARKIT_BLENDSHAPES.indexOf('jawOpen');
49
+ const IDX_MOUTH_FUNNEL = ARKIT_BLENDSHAPES.indexOf('mouthFunnel');
50
+ const IDX_MOUTH_LOWER_DOWN_LEFT = ARKIT_BLENDSHAPES.indexOf('mouthLowerDownLeft');
51
+ const IDX_MOUTH_LOWER_DOWN_RIGHT = ARKIT_BLENDSHAPES.indexOf('mouthLowerDownRight');
2074
52
  class AudioPlayer extends eventemitter3.EventEmitter {
2075
53
  constructor(sampleRate = 24000) {
2076
54
  super();
2077
55
  this.audioContext = null;
2078
56
  this.audioQueue = [];
2079
57
  this.isPlaying = false;
58
+ this.animationFrameId = null;
2080
59
  this.sampleRate = sampleRate;
2081
60
  }
2082
61
  async ensureAudioContext() {
@@ -2089,16 +68,18 @@ class AudioPlayer extends eventemitter3.EventEmitter {
2089
68
  return this.audioContext;
2090
69
  }
2091
70
  enqueue(chunk) {
2092
- console.log('[AudioPlayer] Enqueue called, queue length before:', this.audioQueue.length, 'isPlaying:', this.isPlaying);
2093
71
  this.audioQueue.push(chunk);
2094
72
  if (!this.isPlaying) {
2095
- console.log('[AudioPlayer] Not playing, starting playNext');
2096
73
  this.playNext();
2097
74
  }
2098
75
  }
2099
76
  clearQueue() {
2100
77
  this.audioQueue = [];
2101
78
  this.isPlaying = false;
79
+ if (this.animationFrameId !== null) {
80
+ cancelAnimationFrame(this.animationFrameId);
81
+ this.animationFrameId = null;
82
+ }
2102
83
  this.emit('blendshapeUpdate', new Array(BLENDSHAPE_COUNT).fill(0));
2103
84
  }
2104
85
  async playNext() {
@@ -2125,33 +106,28 @@ class AudioPlayer extends eventemitter3.EventEmitter {
2125
106
  const jawOpen = Math.min(amplitude * 3, 1);
2126
107
  const mouthOpen = Math.min(amplitude * 2.5, 0.8);
2127
108
  const frameData = new Array(BLENDSHAPE_COUNT).fill(0);
2128
- frameData[25] = jawOpen; // jawOpen
2129
- frameData[27] = mouthOpen; // mouthFunnel
2130
- frameData[29] = mouthOpen * 0.3; // mouthLowerDownLeft
2131
- frameData[30] = mouthOpen * 0.3; // mouthLowerDownRight
109
+ frameData[IDX_JAW_OPEN] = jawOpen;
110
+ frameData[IDX_MOUTH_FUNNEL] = mouthOpen;
111
+ frameData[IDX_MOUTH_LOWER_DOWN_LEFT] = mouthOpen * 0.3;
112
+ frameData[IDX_MOUTH_LOWER_DOWN_RIGHT] = mouthOpen * 0.3;
2132
113
  blendshapes.push(frameData);
2133
114
  }
2134
115
  return blendshapes;
2135
116
  }
2136
117
  async playChunk(chunk) {
2137
118
  try {
2138
- console.log('[AudioPlayer] playChunk called, audio byteLength:', chunk.audio.byteLength);
2139
119
  const ctx = await this.ensureAudioContext();
2140
- console.log('[AudioPlayer] AudioContext state:', ctx.state, 'sampleRate:', ctx.sampleRate);
2141
120
  const int16Array = new Int16Array(chunk.audio);
2142
121
  const floatArray = new Float32Array(int16Array.length);
2143
122
  for (let i = 0; i < int16Array.length; i++) {
2144
123
  floatArray[i] = int16Array[i] / 32768.0;
2145
124
  }
2146
- console.log('[AudioPlayer] Converted to float, samples:', floatArray.length);
2147
125
  const audioBuffer = ctx.createBuffer(1, floatArray.length, this.sampleRate);
2148
126
  audioBuffer.getChannelData(0).set(floatArray);
2149
127
  const source = ctx.createBufferSource();
2150
128
  source.buffer = audioBuffer;
2151
129
  source.connect(ctx.destination);
2152
- console.log('[AudioPlayer] Audio buffer duration:', audioBuffer.duration, 'seconds');
2153
130
  const hasBlendshapes = chunk.blendshapes && chunk.blendshapes.length > 0;
2154
- console.log('[AudioPlayer] Has blendshapes:', hasBlendshapes, 'generating amplitude fallback:', !hasBlendshapes);
2155
131
  const blendshapes = hasBlendshapes
2156
132
  ? chunk.blendshapes
2157
133
  : this.generateAmplitudeBlendshapes(floatArray);
@@ -2161,8 +137,13 @@ class AudioPlayer extends eventemitter3.EventEmitter {
2161
137
  this.isPlaying = true;
2162
138
  this.emit('playbackStarted');
2163
139
  const animate = () => {
140
+ if (!this.isPlaying) {
141
+ this.animationFrameId = null;
142
+ return;
143
+ }
2164
144
  const elapsed = performance.now() - startTime;
2165
145
  if (elapsed >= duration || frameIndex >= blendshapes.length) {
146
+ this.animationFrameId = null;
2166
147
  this.emit('blendshapeUpdate', new Array(BLENDSHAPE_COUNT).fill(0));
2167
148
  this.emit('playbackEnded');
2168
149
  this.playNext();
@@ -2177,11 +158,10 @@ class AudioPlayer extends eventemitter3.EventEmitter {
2177
158
  this.emit('blendshapeUpdate', frame);
2178
159
  }
2179
160
  }
2180
- requestAnimationFrame(animate);
161
+ this.animationFrameId = requestAnimationFrame(animate);
2181
162
  };
2182
- console.log('[AudioPlayer] Starting audio playback');
2183
163
  source.start();
2184
- requestAnimationFrame(animate);
164
+ this.animationFrameId = requestAnimationFrame(animate);
2185
165
  }
2186
166
  catch (err) {
2187
167
  console.error('[AudioPlayer] Error in playChunk:', err);
@@ -2212,15 +192,24 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2212
192
  this.imageCaptureInterval = null;
2213
193
  this.callStatus = 'idle';
2214
194
  this.currentBlendshapes = new Array(BLENDSHAPE_COUNT).fill(0);
195
+ this.reconnectAttempts = 0;
196
+ this.reconnectTimer = null;
197
+ this.intentionalClose = false;
198
+ this.hasConnected = false;
199
+ this.clientId = null;
200
+ this.pendingAuth = null;
2215
201
  this.config = {
2216
202
  workerUrl: config.workerUrl,
2217
203
  sessionToken: config.sessionToken || '',
2218
- sessionId: config.sessionId || '',
2219
204
  sampleRate: config.sampleRate || 24000,
2220
- providers: config.providers || { asr: 'self', llm: 'self', tts: 'self' },
2221
205
  fps: config.fps || 30,
2222
206
  enableImageCapture: config.enableImageCapture ?? true,
2223
- imageCaptureInterval: config.imageCaptureInterval || 5000
207
+ imageCaptureInterval: config.imageCaptureInterval || 5000,
208
+ autoReconnect: config.autoReconnect ?? true,
209
+ maxRetries: config.maxRetries ?? 5,
210
+ reconnectDelay: config.reconnectDelay ?? 1000,
211
+ audioProcessorUrl: config.audioProcessorUrl || '/audio-processor.js',
212
+ debug: config.debug ?? false,
2224
213
  };
2225
214
  this.audioPlayer = new AudioPlayer(this.config.sampleRate);
2226
215
  this.setupAudioPlayerEvents();
@@ -2241,32 +230,93 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2241
230
  });
2242
231
  }
2243
232
  async connect() {
233
+ this.intentionalClose = false;
234
+ return this.connectInternal(false);
235
+ }
236
+ connectInternal(isReconnect) {
2244
237
  return new Promise((resolve, reject) => {
2245
238
  const wsUrl = this.config.workerUrl.replace(/^http/, 'ws');
2246
239
  const url = new URL(wsUrl);
2247
- if (this.config.sessionToken) {
2248
- url.searchParams.set('token', this.config.sessionToken);
2249
- }
2250
- if (this.config.sessionId) {
2251
- url.searchParams.set('sessionId', this.config.sessionId);
240
+ if (url.protocol === 'ws:') {
241
+ console.warn('[CSR] Connecting over unencrypted ws:// — use wss:// in production');
2252
242
  }
243
+ const timeout = setTimeout(() => {
244
+ if (this.ws) {
245
+ this.ws.close();
246
+ this.ws = null;
247
+ }
248
+ const error = new Error('WebSocket connection timeout');
249
+ this.emit('error', error);
250
+ if (!isReconnect)
251
+ reject(error);
252
+ }, 30000);
2253
253
  this.ws = new WebSocket(url.toString());
2254
254
  this.ws.onopen = () => {
2255
- this.emit('connected');
2256
- resolve();
255
+ this.pendingAuth = {
256
+ resolve: () => {
257
+ clearTimeout(timeout);
258
+ this.reconnectAttempts = 0;
259
+ this.hasConnected = true;
260
+ if (isReconnect) {
261
+ if (this.callStatus === 'active' || this.callStatus === 'starting') {
262
+ this.stopCall();
263
+ this.emit('callStopped');
264
+ }
265
+ this.emit('reconnected');
266
+ }
267
+ else {
268
+ this.emit('connected');
269
+ }
270
+ resolve();
271
+ },
272
+ reject: (err) => {
273
+ clearTimeout(timeout);
274
+ this.pendingAuth = null;
275
+ if (this.ws) {
276
+ this.ws.close();
277
+ this.ws = null;
278
+ }
279
+ this.emit('error', err);
280
+ if (!isReconnect)
281
+ reject(err);
282
+ }
283
+ };
284
+ this.sendAuthentication();
2257
285
  };
2258
286
  this.ws.onclose = () => {
2259
- this.emit('disconnected');
2260
- this.callStatus = 'idle';
287
+ clearTimeout(timeout);
288
+ this.ws = null;
289
+ if (this.pendingAuth) {
290
+ this.pendingAuth.reject(new Error('WebSocket closed before authentication completed'));
291
+ this.pendingAuth = null;
292
+ }
293
+ if (this.intentionalClose) {
294
+ this.stopCall();
295
+ this.emit('disconnected');
296
+ return;
297
+ }
298
+ if (this.hasConnected && this.config.autoReconnect) {
299
+ this.attemptReconnect();
300
+ }
301
+ else {
302
+ this.stopCall();
303
+ this.emit('disconnected');
304
+ }
2261
305
  };
2262
- this.ws.onerror = (event) => {
306
+ this.ws.onerror = (_event) => {
307
+ clearTimeout(timeout);
2263
308
  const error = new Error('WebSocket connection error');
2264
309
  this.emit('error', error);
2265
- reject(error);
310
+ if (!isReconnect)
311
+ reject(error);
2266
312
  };
2267
313
  this.ws.onmessage = (event) => {
2268
314
  try {
2269
315
  const data = JSON.parse(event.data);
316
+ if (!data || typeof data !== 'object' || typeof data.type !== 'string') {
317
+ console.error('[CSR] Invalid message: missing type');
318
+ return;
319
+ }
2270
320
  this.handleMessage(data);
2271
321
  }
2272
322
  catch (err) {
@@ -2275,43 +325,123 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2275
325
  };
2276
326
  });
2277
327
  }
328
+ sendAuthentication() {
329
+ if (!this.config.sessionToken) {
330
+ if (this.pendingAuth) {
331
+ this.pendingAuth.reject(new Error('Missing sessionToken in config'));
332
+ }
333
+ return;
334
+ }
335
+ if (this.ws) {
336
+ this.ws.send(JSON.stringify({
337
+ type: 'authenticate',
338
+ sessionToken: this.config.sessionToken
339
+ }));
340
+ }
341
+ }
342
+ attemptReconnect() {
343
+ if (this.reconnectAttempts >= this.config.maxRetries) {
344
+ this.stopCall();
345
+ this.emit('error', new Error('Maximum reconnection attempts reached'));
346
+ this.emit('disconnected');
347
+ return;
348
+ }
349
+ this.reconnectAttempts++;
350
+ const delay = this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
351
+ this.emit('reconnecting', this.reconnectAttempts);
352
+ this.reconnectTimer = setTimeout(async () => {
353
+ this.reconnectTimer = null;
354
+ try {
355
+ await this.connectInternal(true);
356
+ }
357
+ catch {
358
+ this.attemptReconnect();
359
+ }
360
+ }, delay);
361
+ }
2278
362
  handleMessage(data) {
2279
- console.log('[CSR] Received message:', data.type);
363
+ if (this.config.debug)
364
+ console.log('[CSR] Received message:', data.type);
2280
365
  switch (data.type) {
2281
- case 'a2f_call_started':
366
+ case 'connected':
367
+ if (typeof data.clientId === 'string')
368
+ this.clientId = data.clientId;
369
+ break;
370
+ case 'authenticated':
371
+ if (this.pendingAuth) {
372
+ this.pendingAuth.resolve();
373
+ this.pendingAuth = null;
374
+ }
375
+ break;
376
+ case 'auth_error': {
377
+ const msg = typeof data.message === 'string' ? data.message : 'Authentication failed';
378
+ if (this.pendingAuth) {
379
+ this.pendingAuth.reject(new Error(msg));
380
+ this.pendingAuth = null;
381
+ }
382
+ else {
383
+ this.emit('error', new Error(msg));
384
+ }
385
+ break;
386
+ }
387
+ case 'call_started':
2282
388
  this.callStatus = 'active';
2283
389
  this.emit('callStarted');
2284
390
  break;
2285
- case 'a2f_call_stopped':
391
+ case 'call_stopped':
2286
392
  this.callStatus = 'idle';
2287
393
  this.emit('callStopped');
2288
394
  break;
2289
- case 'a2f_call_error':
395
+ case 'call_error':
2290
396
  this.callStatus = 'error';
2291
- this.emit('error', new Error(data.message));
397
+ this.emit('error', new Error(typeof data.message === 'string' ? data.message : 'Unknown error'));
2292
398
  break;
2293
- case 'a2f_transcript':
399
+ case 'call_interim':
400
+ if (typeof data.text !== 'string')
401
+ return;
402
+ this.emit('interim', data.text);
403
+ break;
404
+ case 'call_transcript':
405
+ if (typeof data.text !== 'string')
406
+ return;
2294
407
  this.emit('transcript', data.text);
2295
408
  break;
2296
- case 'a2f_response':
409
+ case 'call_response':
410
+ if (typeof data.text !== 'string')
411
+ return;
2297
412
  this.emit('response', data.text);
2298
413
  break;
414
+ case 'call_response_complete':
415
+ this.emit('responseComplete');
416
+ break;
417
+ case 'call_speech_started':
418
+ this.emit('speechStarted');
419
+ break;
420
+ case 'call_utterance_end':
421
+ this.emit('utteranceEnd');
422
+ break;
423
+ case 'call_asr_error':
424
+ this.emit('asrError', typeof data.message === 'string' ? data.message : 'Unknown ASR error');
425
+ break;
2299
426
  case 'call_chunk':
2300
- case 'a2f_call_chunk':
2301
- console.log('[CSR] Audio chunk received, has audio:', !!data.audio, 'has blendshapes:', !!data.blendshapes);
2302
- if (data.audio) {
427
+ if (this.config.debug)
428
+ console.log('[CSR] Audio chunk received, has audio:', !!data.audio, 'has blendshapes:', !!data.blendshapes);
429
+ if (typeof data.audio === 'string') {
2303
430
  try {
2304
431
  const binaryString = atob(data.audio);
2305
432
  const bytes = new Uint8Array(binaryString.length);
2306
433
  for (let i = 0; i < binaryString.length; i++) {
2307
434
  bytes[i] = binaryString.charCodeAt(i);
2308
435
  }
2309
- console.log('[CSR] Decoded audio bytes:', bytes.length);
436
+ if (this.config.debug)
437
+ console.log('[CSR] Decoded audio bytes:', bytes.length);
438
+ const blendshapes = Array.isArray(data.blendshapes) ? data.blendshapes : undefined;
2310
439
  const chunk = {
2311
440
  audio: bytes.buffer,
2312
- blendshapes: data.blendshapes
441
+ blendshapes
2313
442
  };
2314
- console.log('[CSR] Enqueueing audio chunk');
443
+ if (this.config.debug)
444
+ console.log('[CSR] Enqueueing audio chunk');
2315
445
  this.audioPlayer.enqueue(chunk);
2316
446
  }
2317
447
  catch (err) {
@@ -2322,15 +452,15 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2322
452
  console.warn('[CSR] Audio chunk missing audio data');
2323
453
  }
2324
454
  break;
2325
- case 'a2f_model_status':
455
+ case 'model_status':
2326
456
  this.emit('modelStatus', {
2327
- model_loaded: data.model_loaded,
2328
- blendshape_count: data.blendshape_count
457
+ model_loaded: typeof data.model_loaded === 'boolean' ? data.model_loaded : undefined,
458
+ blendshape_count: typeof data.blendshape_count === 'number' ? data.blendshape_count : undefined
2329
459
  });
2330
460
  break;
2331
461
  }
2332
462
  }
2333
- async startCall() {
463
+ async startCall(options) {
2334
464
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
2335
465
  throw new Error('WebSocket not connected');
2336
466
  }
@@ -2341,7 +471,7 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2341
471
  this.audioPlayer.clearQueue();
2342
472
  try {
2343
473
  this.audioContext = new AudioContext({ sampleRate: 16000 });
2344
- await this.audioContext.audioWorklet.addModule('/audio-processor.js');
474
+ await this.audioContext.audioWorklet.addModule(this.config.audioProcessorUrl);
2345
475
  this.mediaStream = await navigator.mediaDevices.getUserMedia({
2346
476
  audio: {
2347
477
  sampleRate: 16000,
@@ -2369,17 +499,22 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2369
499
  }
2370
500
  }
2371
501
  this.ws.send(JSON.stringify({
2372
- type: 'a2f_call_start',
2373
- providers: this.config.providers,
2374
- fps: this.config.fps
502
+ type: 'call_start',
503
+ fps: this.config.fps,
504
+ ...options
2375
505
  }));
2376
506
  const source = this.audioContext.createMediaStreamSource(this.mediaStream);
2377
507
  this.audioProcessor = new AudioWorkletNode(this.audioContext, 'audio-processor');
2378
508
  this.audioProcessor.port.onmessage = (event) => {
2379
509
  if (event.data.type === 'audio_data' && this.ws?.readyState === WebSocket.OPEN) {
2380
- const base64String = btoa(String.fromCharCode(...new Uint8Array(event.data.audioBuffer)));
510
+ const bytes = new Uint8Array(event.data.audioBuffer);
511
+ let binary = '';
512
+ for (let i = 0; i < bytes.length; i += 8192) {
513
+ binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192));
514
+ }
515
+ const base64String = btoa(binary);
2381
516
  this.ws.send(JSON.stringify({
2382
- type: 'a2f_call_audio',
517
+ type: 'call_audio',
2383
518
  audio: base64String
2384
519
  }));
2385
520
  }
@@ -2389,6 +524,22 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2389
524
  }
2390
525
  catch (err) {
2391
526
  this.callStatus = 'error';
527
+ if (this.ws?.readyState === WebSocket.OPEN) {
528
+ this.ws.send(JSON.stringify({ type: 'call_stop' }));
529
+ }
530
+ if (this.mediaStream) {
531
+ this.mediaStream.getTracks().forEach(track => track.stop());
532
+ this.mediaStream = null;
533
+ }
534
+ if (this.audioContext) {
535
+ this.audioContext.close();
536
+ this.audioContext = null;
537
+ }
538
+ if (this.videoRef) {
539
+ this.videoRef.srcObject = null;
540
+ this.videoRef = null;
541
+ }
542
+ this.imageCaptureCanvas = null;
2392
543
  throw err;
2393
544
  }
2394
545
  }
@@ -2416,7 +567,7 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2416
567
  const imageDataUrl = this.imageCaptureCanvas.toDataURL('image/jpeg', 0.7);
2417
568
  const base64Image = imageDataUrl.split(',')[1];
2418
569
  this.ws.send(JSON.stringify({
2419
- type: 'a2f_call_image',
570
+ type: 'call_image',
2420
571
  image: base64Image,
2421
572
  timestamp: Date.now()
2422
573
  }));
@@ -2427,7 +578,7 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2427
578
  this.imageCaptureInterval = null;
2428
579
  }
2429
580
  if (this.ws?.readyState === WebSocket.OPEN) {
2430
- this.ws.send(JSON.stringify({ type: 'a2f_call_stop' }));
581
+ this.ws.send(JSON.stringify({ type: 'call_stop' }));
2431
582
  }
2432
583
  if (this.videoRef) {
2433
584
  this.videoRef.srcObject = null;
@@ -2456,17 +607,39 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2456
607
  throw new Error('WebSocket not connected');
2457
608
  }
2458
609
  this.ws.send(JSON.stringify({
2459
- type: 'a2f_text_message',
610
+ type: 'call_text_input',
2460
611
  text: message
2461
612
  }));
2462
613
  }
2463
- disconnect() {
614
+ async disconnect() {
615
+ this.intentionalClose = true;
616
+ if (this.reconnectTimer) {
617
+ clearTimeout(this.reconnectTimer);
618
+ this.reconnectTimer = null;
619
+ }
620
+ this.reconnectAttempts = 0;
2464
621
  this.stopCall();
2465
622
  if (this.ws) {
623
+ await this.waitForFlush(this.ws);
2466
624
  this.ws.close();
2467
625
  this.ws = null;
2468
626
  }
2469
627
  }
628
+ waitForFlush(ws, timeoutMs = 100) {
629
+ if (ws.bufferedAmount === 0)
630
+ return Promise.resolve();
631
+ return new Promise((resolve) => {
632
+ const start = Date.now();
633
+ const check = () => {
634
+ if (ws.bufferedAmount === 0 || Date.now() - start >= timeoutMs) {
635
+ resolve();
636
+ return;
637
+ }
638
+ setTimeout(check, 10);
639
+ };
640
+ check();
641
+ });
642
+ }
2470
643
  getCallStatus() {
2471
644
  return this.callStatus;
2472
645
  }
@@ -2476,8 +649,11 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2476
649
  isConnected() {
2477
650
  return this.ws?.readyState === WebSocket.OPEN;
2478
651
  }
652
+ getReconnectAttempts() {
653
+ return this.reconnectAttempts;
654
+ }
2479
655
  async destroy() {
2480
- this.disconnect();
656
+ await this.disconnect();
2481
657
  await this.audioPlayer.destroy();
2482
658
  this.removeAllListeners();
2483
659
  }
@@ -2527,7 +703,7 @@ function useAStackCSR(options) {
2527
703
  client.destroy();
2528
704
  clientRef.current = null;
2529
705
  };
2530
- }, [options.workerUrl]);
706
+ }, [options.workerUrl, options.sessionToken]);
2531
707
  const connect = react.useCallback(async () => {
2532
708
  if (clientRef.current) {
2533
709
  await clientRef.current.connect();
@@ -2792,7 +968,7 @@ function TalkingHeadAvatar({ blendshapes, width = 400, height = 400, avatarUrl =
2792
968
  "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/"
2793
969
  }
2794
970
  }
2795
- <\/script>
971
+ ${"</"}script>
2796
972
  </head>
2797
973
  <body>
2798
974
  <div id="avatar"></div>
@@ -2881,7 +1057,7 @@ function TalkingHeadAvatar({ blendshapes, width = 400, height = 400, avatarUrl =
2881
1057
  });
2882
1058
 
2883
1059
  init();
2884
- <\/script>
1060
+ ${"</"}script>
2885
1061
  </body>
2886
1062
  </html>
2887
1063
  `)}`;
@@ -2890,6 +1066,4 @@ function TalkingHeadAvatar({ blendshapes, width = 400, height = 400, avatarUrl =
2890
1066
 
2891
1067
  exports.TalkingHeadAvatar = TalkingHeadAvatar;
2892
1068
  exports.VRMAvatar = VRMAvatar;
2893
- exports.useAStack = useAStack;
2894
1069
  exports.useAStackCSR = useAStackCSR;
2895
- //# sourceMappingURL=react.js.map