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

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