@aether-stack-dev/client-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +257 -0
  2. package/dist/AStackCSRClient.d.ts +60 -0
  3. package/dist/AStackCSRClient.d.ts.map +1 -0
  4. package/dist/AStackClient.d.ts +90 -0
  5. package/dist/AStackClient.d.ts.map +1 -0
  6. package/dist/AStackEventSetup.d.ts +35 -0
  7. package/dist/AStackEventSetup.d.ts.map +1 -0
  8. package/dist/AStackMediaController.d.ts +34 -0
  9. package/dist/AStackMediaController.d.ts.map +1 -0
  10. package/dist/AnalyticsCollector.d.ts +63 -0
  11. package/dist/AnalyticsCollector.d.ts.map +1 -0
  12. package/dist/BillingMonitor.d.ts +35 -0
  13. package/dist/BillingMonitor.d.ts.map +1 -0
  14. package/dist/ConnectionStateManager.d.ts +48 -0
  15. package/dist/ConnectionStateManager.d.ts.map +1 -0
  16. package/dist/PerformanceMonitor.d.ts +34 -0
  17. package/dist/PerformanceMonitor.d.ts.map +1 -0
  18. package/dist/SecurityLogger.d.ts +30 -0
  19. package/dist/SecurityLogger.d.ts.map +1 -0
  20. package/dist/SessionManager.d.ts +20 -0
  21. package/dist/SessionManager.d.ts.map +1 -0
  22. package/dist/SupabaseSignalingClient.d.ts +35 -0
  23. package/dist/SupabaseSignalingClient.d.ts.map +1 -0
  24. package/dist/UsageTracker.d.ts +22 -0
  25. package/dist/UsageTracker.d.ts.map +1 -0
  26. package/dist/WebRTCManager.d.ts +26 -0
  27. package/dist/WebRTCManager.d.ts.map +1 -0
  28. package/dist/__tests__/setup.d.ts +32 -0
  29. package/dist/__tests__/setup.d.ts.map +1 -0
  30. package/dist/audio/AudioPlayer.d.ts +27 -0
  31. package/dist/audio/AudioPlayer.d.ts.map +1 -0
  32. package/dist/audio/index.d.ts +3 -0
  33. package/dist/audio/index.d.ts.map +1 -0
  34. package/dist/avatar/TalkingHeadAvatar.d.ts +9 -0
  35. package/dist/avatar/TalkingHeadAvatar.d.ts.map +1 -0
  36. package/dist/avatar/VRMAvatar.d.ts +10 -0
  37. package/dist/avatar/VRMAvatar.d.ts.map +1 -0
  38. package/dist/avatar/constants.d.ts +4 -0
  39. package/dist/avatar/constants.d.ts.map +1 -0
  40. package/dist/avatar/index.d.ts +7 -0
  41. package/dist/avatar/index.d.ts.map +1 -0
  42. package/dist/core.d.ts +14 -0
  43. package/dist/core.d.ts.map +1 -0
  44. package/dist/index.d.ts +22 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.esm.js +2245 -0
  47. package/dist/index.esm.js.map +1 -0
  48. package/dist/index.js +2258 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/react/index.d.ts +9 -0
  51. package/dist/react/index.d.ts.map +1 -0
  52. package/dist/react/useAStack.d.ts +34 -0
  53. package/dist/react/useAStack.d.ts.map +1 -0
  54. package/dist/react/useAStackCSR.d.ts +20 -0
  55. package/dist/react/useAStackCSR.d.ts.map +1 -0
  56. package/dist/react.esm.js +2871 -0
  57. package/dist/react.esm.js.map +1 -0
  58. package/dist/react.js +2895 -0
  59. package/dist/react.js.map +1 -0
  60. package/dist/types.d.ts +221 -0
  61. package/dist/types.d.ts.map +1 -0
  62. package/package.json +88 -0
@@ -0,0 +1,2871 @@
1
+ import { useRef, useState, useEffect, useCallback } from 'react';
2
+ import { EventEmitter } from 'eventemitter3';
3
+ import { createClient } from '@supabase/supabase-js';
4
+ import { jsxs, jsx } from 'react/jsx-runtime';
5
+ import * as THREE from 'three';
6
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
7
+ import { VRMLoaderPlugin, VRMHumanBoneName } from '@pixiv/three-vrm';
8
+
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
+ const ARKIT_BLENDSHAPES = [
2035
+ 'jawOpen', 'jawForward', 'jawLeft', 'jawRight',
2036
+ 'mouthClose', 'mouthFunnel', 'mouthPucker', 'mouthLeft', 'mouthRight',
2037
+ 'mouthSmileLeft', 'mouthSmileRight', 'mouthFrownLeft', 'mouthFrownRight',
2038
+ 'mouthDimpleLeft', 'mouthDimpleRight', 'mouthStretchLeft', 'mouthStretchRight',
2039
+ 'mouthRollLower', 'mouthRollUpper', 'mouthShrugLower', 'mouthShrugUpper',
2040
+ 'mouthPressLeft', 'mouthPressRight', 'mouthLowerDownLeft', 'mouthLowerDownRight',
2041
+ 'mouthUpperUpLeft', 'mouthUpperUpRight',
2042
+ 'tongueOut',
2043
+ 'cheekPuff', 'cheekSquintLeft', 'cheekSquintRight',
2044
+ 'noseSneerLeft', 'noseSneerRight',
2045
+ 'eyeBlinkLeft', 'eyeBlinkRight',
2046
+ 'eyeLookDownLeft', 'eyeLookDownRight', 'eyeLookInLeft', 'eyeLookInRight',
2047
+ 'eyeLookOutLeft', 'eyeLookOutRight', 'eyeLookUpLeft', 'eyeLookUpRight',
2048
+ 'eyeSquintLeft', 'eyeSquintRight', 'eyeWideLeft', 'eyeWideRight',
2049
+ 'browDownLeft', 'browDownRight', 'browInnerUp', 'browOuterUpLeft', 'browOuterUpRight'
2050
+ ];
2051
+ const BLENDSHAPE_COUNT = 52;
2052
+
2053
+ class AudioPlayer extends EventEmitter {
2054
+ constructor(sampleRate = 24000) {
2055
+ super();
2056
+ this.audioContext = null;
2057
+ this.audioQueue = [];
2058
+ this.isPlaying = false;
2059
+ this.sampleRate = sampleRate;
2060
+ }
2061
+ async ensureAudioContext() {
2062
+ if (!this.audioContext || this.audioContext.state === 'closed') {
2063
+ this.audioContext = new AudioContext({ sampleRate: this.sampleRate });
2064
+ }
2065
+ if (this.audioContext.state === 'suspended') {
2066
+ await this.audioContext.resume();
2067
+ }
2068
+ return this.audioContext;
2069
+ }
2070
+ enqueue(chunk) {
2071
+ console.log('[AudioPlayer] Enqueue called, queue length before:', this.audioQueue.length, 'isPlaying:', this.isPlaying);
2072
+ this.audioQueue.push(chunk);
2073
+ if (!this.isPlaying) {
2074
+ console.log('[AudioPlayer] Not playing, starting playNext');
2075
+ this.playNext();
2076
+ }
2077
+ }
2078
+ clearQueue() {
2079
+ this.audioQueue = [];
2080
+ this.isPlaying = false;
2081
+ this.emit('blendshapeUpdate', new Array(BLENDSHAPE_COUNT).fill(0));
2082
+ }
2083
+ async playNext() {
2084
+ if (this.audioQueue.length === 0) {
2085
+ this.isPlaying = false;
2086
+ this.emit('queueEmpty');
2087
+ return;
2088
+ }
2089
+ const chunk = this.audioQueue.shift();
2090
+ await this.playChunk(chunk);
2091
+ }
2092
+ generateAmplitudeBlendshapes(floatArray, fps = 30) {
2093
+ const samplesPerFrame = Math.floor(this.sampleRate / fps);
2094
+ const frameCount = Math.ceil(floatArray.length / samplesPerFrame);
2095
+ const blendshapes = [];
2096
+ for (let frame = 0; frame < frameCount; frame++) {
2097
+ const start = frame * samplesPerFrame;
2098
+ const end = Math.min(start + samplesPerFrame, floatArray.length);
2099
+ let sum = 0;
2100
+ for (let i = start; i < end; i++) {
2101
+ sum += Math.abs(floatArray[i]);
2102
+ }
2103
+ const amplitude = sum / (end - start);
2104
+ const jawOpen = Math.min(amplitude * 3, 1);
2105
+ const mouthOpen = Math.min(amplitude * 2.5, 0.8);
2106
+ 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
2111
+ blendshapes.push(frameData);
2112
+ }
2113
+ return blendshapes;
2114
+ }
2115
+ async playChunk(chunk) {
2116
+ try {
2117
+ console.log('[AudioPlayer] playChunk called, audio byteLength:', chunk.audio.byteLength);
2118
+ const ctx = await this.ensureAudioContext();
2119
+ console.log('[AudioPlayer] AudioContext state:', ctx.state, 'sampleRate:', ctx.sampleRate);
2120
+ const int16Array = new Int16Array(chunk.audio);
2121
+ const floatArray = new Float32Array(int16Array.length);
2122
+ for (let i = 0; i < int16Array.length; i++) {
2123
+ floatArray[i] = int16Array[i] / 32768.0;
2124
+ }
2125
+ console.log('[AudioPlayer] Converted to float, samples:', floatArray.length);
2126
+ const audioBuffer = ctx.createBuffer(1, floatArray.length, this.sampleRate);
2127
+ audioBuffer.getChannelData(0).set(floatArray);
2128
+ const source = ctx.createBufferSource();
2129
+ source.buffer = audioBuffer;
2130
+ source.connect(ctx.destination);
2131
+ console.log('[AudioPlayer] Audio buffer duration:', audioBuffer.duration, 'seconds');
2132
+ const hasBlendshapes = chunk.blendshapes && chunk.blendshapes.length > 0;
2133
+ console.log('[AudioPlayer] Has blendshapes:', hasBlendshapes, 'generating amplitude fallback:', !hasBlendshapes);
2134
+ const blendshapes = hasBlendshapes
2135
+ ? chunk.blendshapes
2136
+ : this.generateAmplitudeBlendshapes(floatArray);
2137
+ const duration = audioBuffer.duration * 1000;
2138
+ const startTime = performance.now();
2139
+ let frameIndex = 0;
2140
+ this.isPlaying = true;
2141
+ this.emit('playbackStarted');
2142
+ const animate = () => {
2143
+ const elapsed = performance.now() - startTime;
2144
+ if (elapsed >= duration || frameIndex >= blendshapes.length) {
2145
+ this.emit('blendshapeUpdate', new Array(BLENDSHAPE_COUNT).fill(0));
2146
+ this.emit('playbackEnded');
2147
+ this.playNext();
2148
+ return;
2149
+ }
2150
+ const progress = elapsed / duration;
2151
+ const targetFrame = Math.min(Math.floor(progress * blendshapes.length), blendshapes.length - 1);
2152
+ if (targetFrame !== frameIndex) {
2153
+ frameIndex = targetFrame;
2154
+ const frame = blendshapes[frameIndex];
2155
+ if (frame) {
2156
+ this.emit('blendshapeUpdate', frame);
2157
+ }
2158
+ }
2159
+ requestAnimationFrame(animate);
2160
+ };
2161
+ console.log('[AudioPlayer] Starting audio playback');
2162
+ source.start();
2163
+ requestAnimationFrame(animate);
2164
+ }
2165
+ catch (err) {
2166
+ console.error('[AudioPlayer] Error in playChunk:', err);
2167
+ this.emit('error', err instanceof Error ? err : new Error(String(err)));
2168
+ this.isPlaying = false;
2169
+ this.playNext();
2170
+ }
2171
+ }
2172
+ async destroy() {
2173
+ this.clearQueue();
2174
+ if (this.audioContext && this.audioContext.state !== 'closed') {
2175
+ await this.audioContext.close();
2176
+ }
2177
+ this.audioContext = null;
2178
+ this.removeAllListeners();
2179
+ }
2180
+ }
2181
+
2182
+ class AStackCSRClient extends EventEmitter {
2183
+ constructor(config) {
2184
+ super();
2185
+ this.ws = null;
2186
+ this.audioContext = null;
2187
+ this.audioProcessor = null;
2188
+ this.mediaStream = null;
2189
+ this.videoRef = null;
2190
+ this.imageCaptureCanvas = null;
2191
+ this.imageCaptureInterval = null;
2192
+ this.callStatus = 'idle';
2193
+ this.currentBlendshapes = new Array(BLENDSHAPE_COUNT).fill(0);
2194
+ this.config = {
2195
+ workerUrl: config.workerUrl,
2196
+ sessionToken: config.sessionToken || '',
2197
+ sessionId: config.sessionId || '',
2198
+ sampleRate: config.sampleRate || 24000,
2199
+ providers: config.providers || { asr: 'self', llm: 'self', tts: 'self' },
2200
+ fps: config.fps || 30,
2201
+ enableImageCapture: config.enableImageCapture ?? true,
2202
+ imageCaptureInterval: config.imageCaptureInterval || 5000
2203
+ };
2204
+ this.audioPlayer = new AudioPlayer(this.config.sampleRate);
2205
+ this.setupAudioPlayerEvents();
2206
+ }
2207
+ setupAudioPlayerEvents() {
2208
+ this.audioPlayer.on('blendshapeUpdate', (blendshapes) => {
2209
+ this.currentBlendshapes = blendshapes;
2210
+ this.emit('blendshapeUpdate', blendshapes);
2211
+ });
2212
+ this.audioPlayer.on('playbackStarted', () => {
2213
+ this.emit('playbackStarted');
2214
+ });
2215
+ this.audioPlayer.on('playbackEnded', () => {
2216
+ this.emit('playbackEnded');
2217
+ });
2218
+ this.audioPlayer.on('error', (error) => {
2219
+ this.emit('error', error);
2220
+ });
2221
+ }
2222
+ async connect() {
2223
+ return new Promise((resolve, reject) => {
2224
+ const wsUrl = this.config.workerUrl.replace(/^http/, 'ws');
2225
+ 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);
2231
+ }
2232
+ this.ws = new WebSocket(url.toString());
2233
+ this.ws.onopen = () => {
2234
+ this.emit('connected');
2235
+ resolve();
2236
+ };
2237
+ this.ws.onclose = () => {
2238
+ this.emit('disconnected');
2239
+ this.callStatus = 'idle';
2240
+ };
2241
+ this.ws.onerror = (event) => {
2242
+ const error = new Error('WebSocket connection error');
2243
+ this.emit('error', error);
2244
+ reject(error);
2245
+ };
2246
+ this.ws.onmessage = (event) => {
2247
+ try {
2248
+ const data = JSON.parse(event.data);
2249
+ this.handleMessage(data);
2250
+ }
2251
+ catch (err) {
2252
+ console.error('[CSR] Failed to parse message:', err);
2253
+ }
2254
+ };
2255
+ });
2256
+ }
2257
+ handleMessage(data) {
2258
+ console.log('[CSR] Received message:', data.type);
2259
+ switch (data.type) {
2260
+ case 'a2f_call_started':
2261
+ this.callStatus = 'active';
2262
+ this.emit('callStarted');
2263
+ break;
2264
+ case 'a2f_call_stopped':
2265
+ this.callStatus = 'idle';
2266
+ this.emit('callStopped');
2267
+ break;
2268
+ case 'a2f_call_error':
2269
+ this.callStatus = 'error';
2270
+ this.emit('error', new Error(data.message));
2271
+ break;
2272
+ case 'a2f_transcript':
2273
+ this.emit('transcript', data.text);
2274
+ break;
2275
+ case 'a2f_response':
2276
+ this.emit('response', data.text);
2277
+ break;
2278
+ 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) {
2282
+ try {
2283
+ const binaryString = atob(data.audio);
2284
+ const bytes = new Uint8Array(binaryString.length);
2285
+ for (let i = 0; i < binaryString.length; i++) {
2286
+ bytes[i] = binaryString.charCodeAt(i);
2287
+ }
2288
+ console.log('[CSR] Decoded audio bytes:', bytes.length);
2289
+ const chunk = {
2290
+ audio: bytes.buffer,
2291
+ blendshapes: data.blendshapes
2292
+ };
2293
+ console.log('[CSR] Enqueueing audio chunk');
2294
+ this.audioPlayer.enqueue(chunk);
2295
+ }
2296
+ catch (err) {
2297
+ console.error('[CSR] Error decoding audio chunk:', err);
2298
+ }
2299
+ }
2300
+ else {
2301
+ console.warn('[CSR] Audio chunk missing audio data');
2302
+ }
2303
+ break;
2304
+ case 'a2f_model_status':
2305
+ this.emit('modelStatus', {
2306
+ model_loaded: data.model_loaded,
2307
+ blendshape_count: data.blendshape_count
2308
+ });
2309
+ break;
2310
+ }
2311
+ }
2312
+ async startCall() {
2313
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
2314
+ throw new Error('WebSocket not connected');
2315
+ }
2316
+ if (this.callStatus !== 'idle') {
2317
+ throw new Error('Call already in progress');
2318
+ }
2319
+ this.callStatus = 'starting';
2320
+ this.audioPlayer.clearQueue();
2321
+ try {
2322
+ this.audioContext = new AudioContext({ sampleRate: 16000 });
2323
+ await this.audioContext.audioWorklet.addModule('/audio-processor.js');
2324
+ this.mediaStream = await navigator.mediaDevices.getUserMedia({
2325
+ audio: {
2326
+ sampleRate: 16000,
2327
+ channelCount: 1,
2328
+ echoCancellation: true,
2329
+ noiseSuppression: true
2330
+ },
2331
+ video: this.config.enableImageCapture ? {
2332
+ width: { ideal: 320 },
2333
+ height: { ideal: 240 },
2334
+ facingMode: 'user'
2335
+ } : false
2336
+ });
2337
+ if (this.config.enableImageCapture) {
2338
+ const videoTrack = this.mediaStream.getVideoTracks()[0];
2339
+ if (videoTrack) {
2340
+ this.videoRef = document.createElement('video');
2341
+ this.videoRef.autoplay = true;
2342
+ this.videoRef.playsInline = true;
2343
+ this.videoRef.muted = true;
2344
+ this.videoRef.srcObject = new MediaStream([videoTrack]);
2345
+ await this.videoRef.play();
2346
+ this.imageCaptureCanvas = document.createElement('canvas');
2347
+ this.startImageCapture();
2348
+ }
2349
+ }
2350
+ this.ws.send(JSON.stringify({
2351
+ type: 'a2f_call_start',
2352
+ providers: this.config.providers,
2353
+ fps: this.config.fps
2354
+ }));
2355
+ const source = this.audioContext.createMediaStreamSource(this.mediaStream);
2356
+ this.audioProcessor = new AudioWorkletNode(this.audioContext, 'audio-processor');
2357
+ this.audioProcessor.port.onmessage = (event) => {
2358
+ if (event.data.type === 'audio_data' && this.ws?.readyState === WebSocket.OPEN) {
2359
+ const base64String = btoa(String.fromCharCode(...new Uint8Array(event.data.audioBuffer)));
2360
+ this.ws.send(JSON.stringify({
2361
+ type: 'a2f_call_audio',
2362
+ audio: base64String
2363
+ }));
2364
+ }
2365
+ };
2366
+ source.connect(this.audioProcessor);
2367
+ this.audioProcessor.connect(this.audioContext.destination);
2368
+ }
2369
+ catch (err) {
2370
+ this.callStatus = 'error';
2371
+ throw err;
2372
+ }
2373
+ }
2374
+ startImageCapture() {
2375
+ if (this.imageCaptureInterval) {
2376
+ clearInterval(this.imageCaptureInterval);
2377
+ }
2378
+ this.imageCaptureInterval = setInterval(() => {
2379
+ this.captureAndSendImage();
2380
+ }, this.config.imageCaptureInterval);
2381
+ }
2382
+ captureAndSendImage() {
2383
+ if (!this.videoRef || !this.imageCaptureCanvas || !this.ws)
2384
+ return;
2385
+ if (this.ws.readyState !== WebSocket.OPEN)
2386
+ return;
2387
+ if (this.videoRef.readyState < 2)
2388
+ return;
2389
+ const ctx = this.imageCaptureCanvas.getContext('2d');
2390
+ if (!ctx)
2391
+ return;
2392
+ this.imageCaptureCanvas.width = 320;
2393
+ this.imageCaptureCanvas.height = 240;
2394
+ ctx.drawImage(this.videoRef, 0, 0, 320, 240);
2395
+ const imageDataUrl = this.imageCaptureCanvas.toDataURL('image/jpeg', 0.7);
2396
+ const base64Image = imageDataUrl.split(',')[1];
2397
+ this.ws.send(JSON.stringify({
2398
+ type: 'a2f_call_image',
2399
+ image: base64Image,
2400
+ timestamp: Date.now()
2401
+ }));
2402
+ }
2403
+ stopCall() {
2404
+ if (this.imageCaptureInterval) {
2405
+ clearInterval(this.imageCaptureInterval);
2406
+ this.imageCaptureInterval = null;
2407
+ }
2408
+ if (this.ws?.readyState === WebSocket.OPEN) {
2409
+ this.ws.send(JSON.stringify({ type: 'a2f_call_stop' }));
2410
+ }
2411
+ if (this.videoRef) {
2412
+ this.videoRef.srcObject = null;
2413
+ this.videoRef = null;
2414
+ }
2415
+ if (this.mediaStream) {
2416
+ this.mediaStream.getTracks().forEach(track => track.stop());
2417
+ this.mediaStream = null;
2418
+ }
2419
+ if (this.audioProcessor) {
2420
+ this.audioProcessor.disconnect();
2421
+ this.audioProcessor.port.onmessage = null;
2422
+ this.audioProcessor = null;
2423
+ }
2424
+ if (this.audioContext) {
2425
+ this.audioContext.close();
2426
+ this.audioContext = null;
2427
+ }
2428
+ this.audioPlayer.clearQueue();
2429
+ this.callStatus = 'idle';
2430
+ this.currentBlendshapes = new Array(BLENDSHAPE_COUNT).fill(0);
2431
+ this.emit('blendshapeUpdate', this.currentBlendshapes);
2432
+ }
2433
+ sendText(message) {
2434
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
2435
+ throw new Error('WebSocket not connected');
2436
+ }
2437
+ this.ws.send(JSON.stringify({
2438
+ type: 'a2f_text_message',
2439
+ text: message
2440
+ }));
2441
+ }
2442
+ disconnect() {
2443
+ this.stopCall();
2444
+ if (this.ws) {
2445
+ this.ws.close();
2446
+ this.ws = null;
2447
+ }
2448
+ }
2449
+ getCallStatus() {
2450
+ return this.callStatus;
2451
+ }
2452
+ getCurrentBlendshapes() {
2453
+ return [...this.currentBlendshapes];
2454
+ }
2455
+ isConnected() {
2456
+ return this.ws?.readyState === WebSocket.OPEN;
2457
+ }
2458
+ async destroy() {
2459
+ this.disconnect();
2460
+ await this.audioPlayer.destroy();
2461
+ this.removeAllListeners();
2462
+ }
2463
+ }
2464
+
2465
+ function useAStackCSR(options) {
2466
+ const [isConnected, setIsConnected] = useState(false);
2467
+ const [callStatus, setCallStatus] = useState('idle');
2468
+ const [blendshapes, setBlendshapes] = useState(new Array(BLENDSHAPE_COUNT).fill(0));
2469
+ const [transcript, setTranscript] = useState('');
2470
+ const [response, setResponse] = useState('');
2471
+ const [error, setError] = useState(null);
2472
+ const clientRef = useRef(null);
2473
+ useEffect(() => {
2474
+ const client = new AStackCSRClient(options);
2475
+ clientRef.current = client;
2476
+ client.on('connected', () => {
2477
+ setIsConnected(true);
2478
+ setError(null);
2479
+ });
2480
+ client.on('disconnected', () => {
2481
+ setIsConnected(false);
2482
+ setCallStatus('idle');
2483
+ });
2484
+ client.on('error', (err) => {
2485
+ setError(err);
2486
+ });
2487
+ client.on('callStarted', () => {
2488
+ setCallStatus('active');
2489
+ });
2490
+ client.on('callStopped', () => {
2491
+ setCallStatus('idle');
2492
+ });
2493
+ client.on('blendshapeUpdate', (shapes) => {
2494
+ setBlendshapes(shapes);
2495
+ });
2496
+ client.on('transcript', (text) => {
2497
+ setTranscript(text);
2498
+ });
2499
+ client.on('response', (text) => {
2500
+ setResponse(text);
2501
+ });
2502
+ if (options.autoConnect) {
2503
+ client.connect().catch(setError);
2504
+ }
2505
+ return () => {
2506
+ client.destroy();
2507
+ clientRef.current = null;
2508
+ };
2509
+ }, [options.workerUrl]);
2510
+ const connect = useCallback(async () => {
2511
+ if (clientRef.current) {
2512
+ await clientRef.current.connect();
2513
+ }
2514
+ }, []);
2515
+ const disconnect = useCallback(() => {
2516
+ if (clientRef.current) {
2517
+ clientRef.current.disconnect();
2518
+ }
2519
+ }, []);
2520
+ const startCall = useCallback(async () => {
2521
+ if (clientRef.current) {
2522
+ setTranscript('');
2523
+ setResponse('');
2524
+ await clientRef.current.startCall();
2525
+ }
2526
+ }, []);
2527
+ const stopCall = useCallback(() => {
2528
+ if (clientRef.current) {
2529
+ clientRef.current.stopCall();
2530
+ }
2531
+ }, []);
2532
+ const sendText = useCallback((message) => {
2533
+ if (clientRef.current) {
2534
+ clientRef.current.sendText(message);
2535
+ }
2536
+ }, []);
2537
+ return {
2538
+ client: clientRef.current,
2539
+ isConnected,
2540
+ callStatus,
2541
+ blendshapes,
2542
+ transcript,
2543
+ response,
2544
+ error,
2545
+ connect,
2546
+ disconnect,
2547
+ startCall,
2548
+ stopCall,
2549
+ sendText
2550
+ };
2551
+ }
2552
+
2553
+ function VRMAvatar({ blendshapes, width = 400, height = 400, modelUrl = '/models/avatar.vrm', backgroundColor = 0x1a1a2e }) {
2554
+ const containerRef = useRef(null);
2555
+ const rendererRef = useRef(null);
2556
+ const sceneRef = useRef(null);
2557
+ const cameraRef = useRef(null);
2558
+ const vrmRef = useRef(null);
2559
+ const [loading, setLoading] = useState(true);
2560
+ const [error, setError] = useState(null);
2561
+ const animationFrameRef = useRef(0);
2562
+ const animationTimeRef = useRef(0);
2563
+ const applyBlendshapes = useCallback((vrm, shapes) => {
2564
+ const arkitMap = {};
2565
+ ARKIT_BLENDSHAPES.forEach((name, i) => {
2566
+ arkitMap[name] = shapes[i] || 0;
2567
+ });
2568
+ const mouthOpen = arkitMap.jawOpen || 0;
2569
+ const smile = ((arkitMap.mouthSmileLeft || 0) + (arkitMap.mouthSmileRight || 0)) / 2;
2570
+ const funnel = arkitMap.mouthFunnel || 0;
2571
+ const blinkLeft = arkitMap.eyeBlinkLeft || 0;
2572
+ const blinkRight = arkitMap.eyeBlinkRight || 0;
2573
+ if (vrm.expressionManager) {
2574
+ vrm.expressionManager.expressions.forEach(exp => {
2575
+ vrm.expressionManager?.setValue(exp.expressionName, 0);
2576
+ });
2577
+ const trySetExpression = (name, value) => {
2578
+ try {
2579
+ vrm.expressionManager?.setValue(name, Math.min(Math.max(value, 0), 1));
2580
+ }
2581
+ catch {
2582
+ // Expression doesn't exist
2583
+ }
2584
+ };
2585
+ trySetExpression('aa', mouthOpen * 1.5);
2586
+ trySetExpression('happy', smile * 2);
2587
+ trySetExpression('blink', (blinkLeft + blinkRight) / 2);
2588
+ trySetExpression('blinkLeft', blinkLeft);
2589
+ trySetExpression('blinkRight', blinkRight);
2590
+ trySetExpression('ou', funnel);
2591
+ vrm.expressionManager.update();
2592
+ }
2593
+ vrm.scene.traverse((obj) => {
2594
+ if (obj.isMesh) {
2595
+ const mesh = obj;
2596
+ const morphDict = mesh.morphTargetDictionary;
2597
+ const morphInfluences = mesh.morphTargetInfluences;
2598
+ if (morphDict && morphInfluences) {
2599
+ ARKIT_BLENDSHAPES.forEach((name, i) => {
2600
+ const idx = morphDict[name] ?? morphDict[name.toLowerCase()];
2601
+ if (idx !== undefined) {
2602
+ morphInfluences[idx] = shapes[i] || 0;
2603
+ }
2604
+ });
2605
+ }
2606
+ }
2607
+ });
2608
+ }, []);
2609
+ useEffect(() => {
2610
+ const container = containerRef.current;
2611
+ if (!container)
2612
+ return;
2613
+ const renderer = new THREE.WebGLRenderer({
2614
+ antialias: true,
2615
+ alpha: true,
2616
+ powerPreference: 'high-performance'
2617
+ });
2618
+ renderer.setSize(width, height);
2619
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
2620
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
2621
+ container.appendChild(renderer.domElement);
2622
+ rendererRef.current = renderer;
2623
+ const scene = new THREE.Scene();
2624
+ scene.background = new THREE.Color(backgroundColor);
2625
+ sceneRef.current = scene;
2626
+ const camera = new THREE.PerspectiveCamera(30, width / height, 0.1, 20);
2627
+ camera.position.set(0, 1.4, 1.2);
2628
+ camera.lookAt(0, 1.3, 0);
2629
+ cameraRef.current = camera;
2630
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
2631
+ scene.add(ambientLight);
2632
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
2633
+ directionalLight.position.set(1, 1, 1);
2634
+ scene.add(directionalLight);
2635
+ const backLight = new THREE.DirectionalLight(0x4a9eff, 0.3);
2636
+ backLight.position.set(-1, 1, -1);
2637
+ scene.add(backLight);
2638
+ const loader = new GLTFLoader();
2639
+ loader.register((parser) => new VRMLoaderPlugin(parser));
2640
+ loader.load(modelUrl, (gltf) => {
2641
+ const vrm = gltf.userData.vrm;
2642
+ if (vrm) {
2643
+ scene.add(vrm.scene);
2644
+ vrmRef.current = vrm;
2645
+ setLoading(false);
2646
+ }
2647
+ }, () => { }, (err) => {
2648
+ console.error('[VRM] Error loading model:', err);
2649
+ setError('Failed to load VRM model');
2650
+ setLoading(false);
2651
+ });
2652
+ const clock = new THREE.Clock();
2653
+ const animate = () => {
2654
+ animationFrameRef.current = requestAnimationFrame(animate);
2655
+ const delta = clock.getDelta();
2656
+ animationTimeRef.current += delta;
2657
+ if (vrmRef.current) {
2658
+ vrmRef.current.update(delta);
2659
+ const humanoid = vrmRef.current.humanoid;
2660
+ if (humanoid) {
2661
+ const time = animationTimeRef.current;
2662
+ const breathCycle = time * 1.2;
2663
+ const breathIntensity = 0.03;
2664
+ const idleSway = time * 0.5;
2665
+ const swayIntensity = 0.02;
2666
+ const headBob = time * 0.7;
2667
+ const headBobIntensity = 0.015;
2668
+ const spineBone = humanoid.getNormalizedBoneNode(VRMHumanBoneName.Spine);
2669
+ if (spineBone) {
2670
+ spineBone.rotation.x = Math.sin(breathCycle) * breathIntensity;
2671
+ spineBone.rotation.z = Math.sin(idleSway) * swayIntensity * 0.3;
2672
+ }
2673
+ const chestBone = humanoid.getNormalizedBoneNode(VRMHumanBoneName.Chest);
2674
+ if (chestBone) {
2675
+ chestBone.rotation.x = Math.sin(breathCycle + 0.3) * breathIntensity * 0.5;
2676
+ chestBone.rotation.z = Math.sin(idleSway + 0.5) * swayIntensity * 0.2;
2677
+ }
2678
+ const neckBone = humanoid.getNormalizedBoneNode(VRMHumanBoneName.Neck);
2679
+ if (neckBone) {
2680
+ neckBone.rotation.x = Math.sin(headBob) * headBobIntensity;
2681
+ neckBone.rotation.y = Math.sin(idleSway * 0.7) * swayIntensity * 0.5;
2682
+ }
2683
+ const headBone = humanoid.getNormalizedBoneNode(VRMHumanBoneName.Head);
2684
+ if (headBone) {
2685
+ headBone.rotation.x = Math.sin(headBob + 0.5) * headBobIntensity * 0.4;
2686
+ headBone.rotation.y = Math.sin(idleSway * 0.6 + 0.8) * swayIntensity * 0.3;
2687
+ }
2688
+ const leftShoulder = humanoid.getNormalizedBoneNode(VRMHumanBoneName.LeftShoulder);
2689
+ const rightShoulder = humanoid.getNormalizedBoneNode(VRMHumanBoneName.RightShoulder);
2690
+ if (leftShoulder) {
2691
+ leftShoulder.rotation.z = Math.sin(breathCycle) * breathIntensity * 0.2;
2692
+ }
2693
+ if (rightShoulder) {
2694
+ rightShoulder.rotation.z = -Math.sin(breathCycle) * breathIntensity * 0.2;
2695
+ }
2696
+ const leftUpperArm = humanoid.getNormalizedBoneNode(VRMHumanBoneName.LeftUpperArm);
2697
+ const rightUpperArm = humanoid.getNormalizedBoneNode(VRMHumanBoneName.RightUpperArm);
2698
+ if (leftUpperArm) {
2699
+ leftUpperArm.rotation.z = -1 + Math.sin(idleSway * 0.4) * 0.02;
2700
+ }
2701
+ if (rightUpperArm) {
2702
+ rightUpperArm.rotation.z = 1.0 + Math.sin(idleSway * 0.4 + 0.5) * 0.02;
2703
+ }
2704
+ }
2705
+ }
2706
+ renderer.render(scene, camera);
2707
+ };
2708
+ animate();
2709
+ return () => {
2710
+ cancelAnimationFrame(animationFrameRef.current);
2711
+ renderer.dispose();
2712
+ if (container && renderer.domElement) {
2713
+ container.removeChild(renderer.domElement);
2714
+ }
2715
+ };
2716
+ }, [width, height, modelUrl, backgroundColor]);
2717
+ useEffect(() => {
2718
+ if (vrmRef.current && blendshapes.length > 0) {
2719
+ applyBlendshapes(vrmRef.current, blendshapes);
2720
+ }
2721
+ }, [blendshapes, applyBlendshapes]);
2722
+ return (jsxs("div", { className: "relative", style: { width, height }, children: [jsx("div", { ref: containerRef, className: "rounded-lg overflow-hidden" }), 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 VRM 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", children: error }) }))] }));
2723
+ }
2724
+
2725
+ function TalkingHeadAvatar({ blendshapes, width = 400, height = 400, avatarUrl = 'https://models.readyplayer.me/6460d95f9ae10f45bffb2864.glb?morphTargets=ARKit,Oculus+Visemes,mouthOpen,mouthSmile,eyesClosed,eyesLookUp,eyesLookDown&textureSizeLimit=1024&textureFormat=png' }) {
2726
+ const iframeRef = useRef(null);
2727
+ const [loading, setLoading] = useState(true);
2728
+ const [error, setError] = useState(null);
2729
+ useEffect(() => {
2730
+ const iframe = iframeRef.current;
2731
+ if (!iframe)
2732
+ return;
2733
+ const handleMessage = (event) => {
2734
+ if (event.data?.type === 'talkinghead-ready') {
2735
+ setLoading(false);
2736
+ }
2737
+ else if (event.data?.type === 'talkinghead-error') {
2738
+ setError(event.data.message || 'Failed to load TalkingHead avatar');
2739
+ setLoading(false);
2740
+ }
2741
+ };
2742
+ window.addEventListener('message', handleMessage);
2743
+ return () => window.removeEventListener('message', handleMessage);
2744
+ }, []);
2745
+ useEffect(() => {
2746
+ const iframe = iframeRef.current;
2747
+ if (!iframe?.contentWindow || blendshapes.length === 0)
2748
+ return;
2749
+ const blendshapeData = {};
2750
+ ARKIT_BLENDSHAPES.forEach((name, i) => {
2751
+ blendshapeData[name] = blendshapes[i] || 0;
2752
+ });
2753
+ iframe.contentWindow.postMessage({
2754
+ type: 'update-blendshapes',
2755
+ blendshapes: blendshapeData
2756
+ }, '*');
2757
+ }, [blendshapes]);
2758
+ const iframeSrc = `data:text/html,${encodeURIComponent(`
2759
+ <!DOCTYPE html>
2760
+ <html>
2761
+ <head>
2762
+ <style>
2763
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2764
+ html, body { width: 100%; height: 100%; overflow: hidden; background: #1a1a2e; }
2765
+ #avatar { width: 100%; height: 100%; }
2766
+ </style>
2767
+ <script type="importmap">
2768
+ {
2769
+ "imports": {
2770
+ "three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
2771
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/"
2772
+ }
2773
+ }
2774
+ <\/script>
2775
+ </head>
2776
+ <body>
2777
+ <div id="avatar"></div>
2778
+ <script type="module">
2779
+ import { TalkingHead } from 'https://cdn.jsdelivr.net/npm/@met4citizen/talkinghead@1.6.0/modules/talkinghead.mjs';
2780
+
2781
+ let head = null;
2782
+
2783
+ async function init() {
2784
+ try {
2785
+ const container = document.getElementById('avatar');
2786
+ head = new TalkingHead(container, {
2787
+ ttsEndpoint: null,
2788
+ cameraView: 'head',
2789
+ cameraRotateEnable: false,
2790
+ cameraPanEnable: false,
2791
+ cameraZoomEnable: false,
2792
+ lightAmbientColor: 0xffffff,
2793
+ lightAmbientIntensity: 0.6,
2794
+ lightDirectColor: 0xffffff,
2795
+ lightDirectIntensity: 0.8,
2796
+ lightSpotIntensity: 0,
2797
+ avatarMood: 'neutral',
2798
+ modelPixelRatio: Math.min(window.devicePixelRatio, 2),
2799
+ modelFPS: 30
2800
+ });
2801
+
2802
+ await head.showAvatar({
2803
+ url: '${avatarUrl}',
2804
+ body: 'F',
2805
+ avatarMood: 'neutral',
2806
+ lipsyncLang: 'en'
2807
+ });
2808
+
2809
+ window.parent.postMessage({ type: 'talkinghead-ready' }, '*');
2810
+ } catch (err) {
2811
+ console.error('[TalkingHead] Error:', err);
2812
+ window.parent.postMessage({ type: 'talkinghead-error', message: err.message }, '*');
2813
+ }
2814
+ }
2815
+
2816
+ let started = false;
2817
+
2818
+ window.addEventListener('message', (event) => {
2819
+ if (event.data?.type === 'update-blendshapes' && head) {
2820
+ if (!started) {
2821
+ head.start();
2822
+ started = true;
2823
+ }
2824
+
2825
+ const shapes = event.data.blendshapes;
2826
+ const mtAvatar = head.mtAvatar;
2827
+
2828
+ if (mtAvatar) {
2829
+ for (const [name, value] of Object.entries(shapes)) {
2830
+ const v = Math.min(Number(value) || 0, 1);
2831
+ if (mtAvatar[name]) {
2832
+ mtAvatar[name].value = v;
2833
+ mtAvatar[name].applied = v;
2834
+ if (mtAvatar[name].ms && mtAvatar[name].is) {
2835
+ for (let i = 0; i < mtAvatar[name].ms.length; i++) {
2836
+ const influences = mtAvatar[name].ms[i];
2837
+ const idx = mtAvatar[name].is[i];
2838
+ if (influences && idx !== undefined) {
2839
+ influences[idx] = v;
2840
+ }
2841
+ }
2842
+ }
2843
+ }
2844
+ }
2845
+ }
2846
+
2847
+ if (head.morphs) {
2848
+ for (const mesh of head.morphs) {
2849
+ if (mesh.morphTargetDictionary && mesh.morphTargetInfluences) {
2850
+ for (const [name, value] of Object.entries(shapes)) {
2851
+ const idx = mesh.morphTargetDictionary[name];
2852
+ if (idx !== undefined) {
2853
+ mesh.morphTargetInfluences[idx] = Math.min(Number(value) || 0, 1);
2854
+ }
2855
+ }
2856
+ }
2857
+ }
2858
+ }
2859
+ }
2860
+ });
2861
+
2862
+ init();
2863
+ <\/script>
2864
+ </body>
2865
+ </html>
2866
+ `)}`;
2867
+ 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
+ }
2869
+
2870
+ export { TalkingHeadAvatar, VRMAvatar, useAStack, useAStackCSR };
2871
+ //# sourceMappingURL=react.esm.js.map